Page MenuHomeMusing Studio

No OneTemporary

diff --git a/admin.go b/admin.go
index 5f7d244..33445ac 100644
--- a/admin.go
+++ b/admin.go
@@ -1,530 +1,575 @@
/*
- * Copyright © 2018-2019 A Bunch Tell LLC.
+ * Copyright © 2018-2020 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package writefreely
import (
"database/sql"
"fmt"
"net/http"
"runtime"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/auth"
"github.com/writeas/web-core/log"
"github.com/writeas/web-core/passgen"
"github.com/writeas/writefreely/appstats"
"github.com/writeas/writefreely/config"
)
var (
appStartTime = time.Now()
sysStatus systemStatus
)
const adminUsersPerPage = 30
type systemStatus struct {
Uptime string
NumGoroutine int
// General statistics.
MemAllocated string // bytes allocated and still in use
MemTotal string // bytes allocated (even if freed)
MemSys string // bytes obtained from system (sum of XxxSys below)
Lookups uint64 // number of pointer lookups
MemMallocs uint64 // number of mallocs
MemFrees uint64 // number of frees
// Main allocation heap statistics.
HeapAlloc string // bytes allocated and still in use
HeapSys string // bytes obtained from system
HeapIdle string // bytes in idle spans
HeapInuse string // bytes in non-idle span
HeapReleased string // bytes released to the OS
HeapObjects uint64 // total number of allocated objects
// Low-level fixed-size structure allocator statistics.
// Inuse is bytes used now.
// Sys is bytes obtained from system.
StackInuse string // bootstrap stacks
StackSys string
MSpanInuse string // mspan structures
MSpanSys string
MCacheInuse string // mcache structures
MCacheSys string
BuckHashSys string // profiling bucket hash table
GCSys string // GC metadata
OtherSys string // other system allocations
// Garbage collector statistics.
NextGC string // next run in HeapAlloc time (bytes)
LastGC string // last run in absolute time (ns)
PauseTotalNs string
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
NumGC uint32
}
type inspectedCollection struct {
CollectionObj
Followers int
LastPost string
}
type instanceContent struct {
ID string
Type string
Title sql.NullString
Content string
Updated time.Time
}
func (c instanceContent) UpdatedFriendly() string {
/*
// TODO: accept a locale in this method and use that for the format
var loc monday.Locale = monday.LocaleEnUS
return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
*/
return c.Updated.Format("January 2, 2006, 3:04 PM")
}
func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
+ p := struct {
+ *UserPage
+ Message string
+
+ UsersCount, CollectionsCount, PostsCount int64
+ }{
+ UserPage: NewUserPage(app, r, u, "Admin", nil),
+ Message: r.FormValue("m"),
+ }
+
+ // Get user stats
+ p.UsersCount = app.db.GetAllUsersCount()
+ var err error
+ p.CollectionsCount, err = app.db.GetTotalCollections()
+ if err != nil {
+ return err
+ }
+ p.PostsCount, err = app.db.GetTotalPosts()
+ if err != nil {
+ return err
+ }
+
+ showUserPage(w, "admin", p)
+ return nil
+}
+
+func handleViewAdminMonitor(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
updateAppStats()
p := struct {
*UserPage
SysStatus systemStatus
Config config.AppCfg
Message, ConfigMessage string
}{
UserPage: NewUserPage(app, r, u, "Admin", nil),
SysStatus: sysStatus,
Config: app.cfg.App,
Message: r.FormValue("m"),
ConfigMessage: r.FormValue("cm"),
}
- showUserPage(w, "admin", p)
+ showUserPage(w, "monitor", p)
+ return nil
+}
+
+func handleViewAdminSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
+ p := struct {
+ *UserPage
+ Config config.AppCfg
+
+ Message, ConfigMessage string
+ }{
+ UserPage: NewUserPage(app, r, u, "Admin", nil),
+ Config: app.cfg.App,
+
+ Message: r.FormValue("m"),
+ ConfigMessage: r.FormValue("cm"),
+ }
+
+ showUserPage(w, "app-settings", p)
return nil
}
func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct {
*UserPage
Config config.AppCfg
Message string
Users *[]User
CurPage int
TotalUsers int64
TotalPages []int
}{
UserPage: NewUserPage(app, r, u, "Users", nil),
Config: app.cfg.App,
Message: r.FormValue("m"),
}
p.TotalUsers = app.db.GetAllUsersCount()
ttlPages := p.TotalUsers / adminUsersPerPage
p.TotalPages = []int{}
for i := 1; i <= int(ttlPages); i++ {
p.TotalPages = append(p.TotalPages, i)
}
var err error
p.CurPage, err = strconv.Atoi(r.FormValue("p"))
if err != nil || p.CurPage < 1 {
p.CurPage = 1
} else if p.CurPage > int(ttlPages) {
p.CurPage = int(ttlPages)
}
p.Users, err = app.db.GetAllUsers(uint(p.CurPage))
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get users: %v", err)}
}
showUserPage(w, "users", p)
return nil
}
func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
p := struct {
*UserPage
Config config.AppCfg
Message string
User *User
Colls []inspectedCollection
LastPost string
NewPassword string
TotalPosts int64
ClearEmail string
}{
Config: app.cfg.App,
Message: r.FormValue("m"),
Colls: []inspectedCollection{},
}
var err error
p.User, err = app.db.GetUserForAuth(username)
if err != nil {
if err == ErrUserNotFound {
return err
}
log.Error("Could not get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, err.Error()}
}
flashes, _ := getSessionFlashes(app, w, r, nil)
for _, flash := range flashes {
if strings.HasPrefix(flash, "SUCCESS: ") {
p.NewPassword = strings.TrimPrefix(flash, "SUCCESS: ")
p.ClearEmail = p.User.EmailClear(app.keys)
}
}
p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
lp, err := app.db.GetUserLastPostTime(p.User.ID)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's last post time: %v", err)}
}
if lp != nil {
p.LastPost = lp.Format("January 2, 2006, 3:04 PM")
}
colls, err := app.db.GetCollections(p.User, app.cfg.App.Host)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)}
}
for _, c := range *colls {
ic := inspectedCollection{
CollectionObj: CollectionObj{Collection: c},
}
if app.cfg.App.Federation {
folls, err := app.db.GetAPFollowers(&c)
if err == nil {
// TODO: handle error here (at least log it)
ic.Followers = len(*folls)
}
}
app.db.GetPostsCount(&ic.CollectionObj, true)
lp, err := app.db.GetCollectionLastPostTime(c.ID)
if err != nil {
log.Error("Didn't get last post time for collection %d: %v", c.ID, err)
}
if lp != nil {
ic.LastPost = lp.Format("January 2, 2006, 3:04 PM")
}
p.Colls = append(p.Colls, ic)
}
showUserPage(w, "view-user", p)
return nil
}
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
user, err := app.db.GetUserForAuth(username)
if err != nil {
log.Error("failed to get user: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
}
if user.IsSilenced() {
err = app.db.SetUserStatus(user.ID, UserActive)
} else {
err = app.db.SetUserStatus(user.ID, UserSilenced)
}
if err != nil {
log.Error("toggle user silenced: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user status: %v", err)}
}
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
}
func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]
if username == "" {
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
// Generate new random password since none supplied
pass := passgen.NewWordish()
hashedPass, err := auth.HashPass([]byte(pass))
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
}
userIDVal := r.FormValue("user")
log.Info("ADMIN: Changing user %s password", userIDVal)
id, err := strconv.Atoi(userIDVal)
if err != nil {
return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Invalid user ID: %v", err)}
}
err = app.db.ChangePassphrase(int64(id), true, "", hashedPass)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
}
log.Info("ADMIN: Successfully changed.")
addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: %s", pass), nil)
return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s", username)}
}
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
p := struct {
*UserPage
Config config.AppCfg
Message string
Pages []*instanceContent
}{
UserPage: NewUserPage(app, r, u, "Pages", nil),
Config: app.cfg.App,
Message: r.FormValue("m"),
}
var err error
p.Pages, err = app.db.GetInstancePages()
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)}
}
// Add in default pages
var hasAbout, hasPrivacy bool
for i, c := range p.Pages {
if hasAbout && hasPrivacy {
break
}
if c.ID == "about" {
hasAbout = true
if !c.Title.Valid {
p.Pages[i].Title = defaultAboutTitle(app.cfg)
}
} else if c.ID == "privacy" {
hasPrivacy = true
if !c.Title.Valid {
p.Pages[i].Title = defaultPrivacyTitle()
}
}
}
if !hasAbout {
p.Pages = append(p.Pages, &instanceContent{
ID: "about",
Title: defaultAboutTitle(app.cfg),
Content: defaultAboutPage(app.cfg),
Updated: defaultPageUpdatedTime,
})
}
if !hasPrivacy {
p.Pages = append(p.Pages, &instanceContent{
ID: "privacy",
Title: defaultPrivacyTitle(),
Content: defaultPrivacyPolicy(app.cfg),
Updated: defaultPageUpdatedTime,
})
}
showUserPage(w, "pages", p)
return nil
}
func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
slug := vars["slug"]
if slug == "" {
return impart.HTTPError{http.StatusFound, "/admin/pages"}
}
p := struct {
*UserPage
Config config.AppCfg
Message string
Banner *instanceContent
Content *instanceContent
}{
Config: app.cfg.App,
Message: r.FormValue("m"),
}
var err error
// Get pre-defined pages, or select slug
if slug == "about" {
p.Content, err = getAboutPage(app)
} else if slug == "privacy" {
p.Content, err = getPrivacyPage(app)
} else if slug == "landing" {
p.Banner, err = getLandingBanner(app)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
}
p.Content, err = getLandingBody(app)
p.Content.ID = "landing"
} else if slug == "reader" {
p.Content, err = getReaderSection(app)
} else {
p.Content, err = app.db.GetDynamicContent(slug)
}
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
}
title := "New page"
if p.Content != nil {
title = "Edit " + p.Content.ID
} else {
p.Content = &instanceContent{}
}
p.UserPage = NewUserPage(app, r, u, title, nil)
showUserPage(w, "view-page", p)
return nil
}
func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["page"]
// Validate
if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
return impart.HTTPError{http.StatusNotFound, "No such page."}
}
var err error
m := ""
if id == "landing" {
// Handle special landing page
err = app.db.UpdateDynamicContent("landing-banner", "", r.FormValue("banner"), "section")
if err != nil {
m = "?m=" + err.Error()
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
}
err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
} else if id == "reader" {
// Update sections with titles
err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section")
} else {
// Update page
err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
}
if err != nil {
m = "?m=" + err.Error()
}
return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
}
func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
apper.App().cfg.App.SiteName = r.FormValue("site_name")
apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
apper.App().cfg.App.Landing = r.FormValue("landing")
apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
mul, err := strconv.Atoi(r.FormValue("min_username_len"))
if err == nil {
apper.App().cfg.App.MinUsernameLen = mul
}
mb, err := strconv.Atoi(r.FormValue("max_blogs"))
if err == nil {
apper.App().cfg.App.MaxBlogs = mb
}
apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
apper.App().cfg.App.Private = r.FormValue("private") == "on"
apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
log.Info("Initializing local timeline...")
initLocalTimeline(apper.App())
}
apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
if apper.App().cfg.App.UserInvites == "none" {
apper.App().cfg.App.UserInvites = ""
}
apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
m := "?cm=Configuration+saved."
err = apper.SaveConfig(apper.App().cfg)
if err != nil {
m = "?cm=" + err.Error()
}
- return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
+ return impart.HTTPError{http.StatusFound, "/admin/settings" + m + "#config"}
}
func updateAppStats() {
sysStatus.Uptime = appstats.TimeSincePro(appStartTime)
m := new(runtime.MemStats)
runtime.ReadMemStats(m)
sysStatus.NumGoroutine = runtime.NumGoroutine()
sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc))
sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc))
sysStatus.MemSys = appstats.FileSize(int64(m.Sys))
sysStatus.Lookups = m.Lookups
sysStatus.MemMallocs = m.Mallocs
sysStatus.MemFrees = m.Frees
sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc))
sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys))
sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle))
sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse))
sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased))
sysStatus.HeapObjects = m.HeapObjects
sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse))
sysStatus.StackSys = appstats.FileSize(int64(m.StackSys))
sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse))
sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys))
sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse))
sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys))
sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys))
sysStatus.GCSys = appstats.FileSize(int64(m.GCSys))
sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys))
sysStatus.NextGC = appstats.FileSize(int64(m.NextGC))
sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
sysStatus.NumGC = m.NumGC
}
func adminResetPassword(app *App, u *User, newPass string) error {
hashedPass, err := auth.HashPass([]byte(newPass))
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
}
err = app.db.ChangePassphrase(u.ID, true, "", hashedPass)
if err != nil {
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
}
return nil
}
diff --git a/less/admin.less b/less/admin.less
index 9c4a7c2..3cbf30b 100644
--- a/less/admin.less
+++ b/less/admin.less
@@ -1,44 +1,44 @@
.edit-page {
font-size: 1em;
min-height: 12em;
}
header.admin {
margin: 0;
h1 + a {
margin-left: 1em;
}
}
nav#admin {
display: block;
margin: 0.5em 0;
a {
- color: @primary;
- &:first-child {
- margin-left: 0;
- }
+ margin-left: 0;
+ .rounded(.25em);
+ border: 0;
&.selected {
+ background: #dedede;
font-weight: bold;
}
}
}
.pager {
display: flex;
justify-content: center;
a {
color: #333;
font-family: @sansFont;
font-size: 0.86em;
padding: 0.5em 1em;
border: 1px solid #ccc;
&:hover {
text-decoration: none;
background: #efefef;
}
&.selected {
cursor: default;
background: #ccc;
}
}
}
diff --git a/routes.go b/routes.go
index fcd00ec..e606fbc 100644
--- a/routes.go
+++ b/routes.go
@@ -1,219 +1,221 @@
/*
* Copyright © 2018-2019 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
package writefreely
import (
"net/http"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log"
"github.com/writefreely/go-nodeinfo"
)
// InitStaticRoutes adds routes for serving static files.
// TODO: this should just be a func, not method
func (app *App) InitStaticRoutes(r *mux.Router) {
// Handle static files
fs := http.FileServer(http.Dir(filepath.Join(app.cfg.Server.StaticParentDir, staticDir)))
app.shttp = http.NewServeMux()
app.shttp.Handle("/", fs)
r.PathPrefix("/").Handler(fs)
}
// InitRoutes adds dynamic routes for the given mux.Router.
func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
// Create handler
handler := NewWFHandler(apper)
// Set up routes
hostSubroute := apper.App().cfg.App.Host[strings.Index(apper.App().cfg.App.Host, "://")+3:]
if apper.App().cfg.App.SingleUser {
hostSubroute = "{domain}"
} else {
if strings.HasPrefix(hostSubroute, "localhost") {
hostSubroute = "localhost"
}
}
if apper.App().cfg.App.SingleUser {
log.Info("Adding %s routes (single user)...", hostSubroute)
} else {
log.Info("Adding %s routes (multi-user)...", hostSubroute)
}
// Primary app routes
write := r.PathPrefix("/").Subrouter()
// Federation endpoint configurations
wf := webfinger.Default(wfResolver{apper.App().db, apper.App().cfg})
wf.NoTLSHandler = nil
// Federation endpoints
// host-meta
write.HandleFunc("/.well-known/host-meta", handler.Web(handleViewHostMeta, UserLevelReader))
// webfinger
write.HandleFunc(webfinger.WebFingerPath, handler.LogHandlerFunc(http.HandlerFunc(wf.Webfinger)))
// nodeinfo
niCfg := nodeInfoConfig(apper.App().db, apper.App().cfg)
ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{apper.App().cfg, apper.App().db})
write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover)))
write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo)))
// handle mentions
write.HandleFunc("/@/{handle}", handler.Web(handleViewMention, UserLevelReader))
configureSlackOauth(handler, write, apper.App())
configureWriteAsOauth(handler, write, apper.App())
// Set up dyamic page handlers
// Handle auth
auth := write.PathPrefix("/api/auth/").Subrouter()
if apper.App().cfg.App.OpenRegistration {
auth.HandleFunc("/signup", handler.All(apiSignup)).Methods("POST")
}
auth.HandleFunc("/login", handler.All(login)).Methods("POST")
auth.HandleFunc("/read", handler.WebErrors(handleWebCollectionUnlock, UserLevelNone)).Methods("POST")
auth.HandleFunc("/me", handler.All(handleAPILogout)).Methods("DELETE")
// Handle logged in user sections
me := write.PathPrefix("/me").Subrouter()
me.HandleFunc("/", handler.Redirect("/me", UserLevelUser))
me.HandleFunc("/c", handler.Redirect("/me/c/", UserLevelUser)).Methods("GET")
me.HandleFunc("/c/", handler.User(viewCollections)).Methods("GET")
me.HandleFunc("/c/{collection}", handler.User(viewEditCollection)).Methods("GET")
me.HandleFunc("/c/{collection}/stats", handler.User(viewStats)).Methods("GET")
me.HandleFunc("/posts", handler.Redirect("/me/posts/", UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/", handler.User(viewArticles)).Methods("GET")
me.HandleFunc("/posts/export.csv", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.zip", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/posts/export.json", handler.Download(viewExportPosts, UserLevelUser)).Methods("GET")
me.HandleFunc("/export", handler.User(viewExportOptions)).Methods("GET")
me.HandleFunc("/export.json", handler.Download(viewExportFull, UserLevelUser)).Methods("GET")
me.HandleFunc("/import", handler.User(viewImport)).Methods("GET")
me.HandleFunc("/settings", handler.User(viewSettings)).Methods("GET")
me.HandleFunc("/invites", handler.User(handleViewUserInvites)).Methods("GET")
me.HandleFunc("/logout", handler.Web(viewLogout, UserLevelNone)).Methods("GET")
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
apiMe := write.PathPrefix("/api/me/").Subrouter()
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
apiMe.HandleFunc("/posts", handler.UserAPI(viewMyPostsAPI)).Methods("GET")
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
apiMe.HandleFunc("/invites", handler.User(handleCreateUserInvite)).Methods("POST")
apiMe.HandleFunc("/import", handler.User(handleImport)).Methods("POST")
// Sign up validation
write.HandleFunc("/api/alias", handler.All(handleUsernameCheck)).Methods("POST")
write.HandleFunc("/api/markdown", handler.All(handleRenderMarkdown)).Methods("POST")
// Handle collections
write.HandleFunc("/api/collections", handler.All(newCollection)).Methods("POST")
apiColls := write.PathPrefix("/api/collections/").Subrouter()
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.AllReader(fetchCollection)).Methods("GET")
apiColls.HandleFunc("/{alias:[0-9a-zA-Z\\-]+}", handler.All(existingCollection)).Methods("POST", "DELETE")
apiColls.HandleFunc("/{alias}/posts", handler.AllReader(fetchCollectionPosts)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts", handler.All(newPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}", handler.AllReader(fetchPost)).Methods("GET")
apiColls.HandleFunc("/{alias}/posts/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/posts/{post}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
apiColls.HandleFunc("/{alias}/collect", handler.All(addPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/pin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/unpin", handler.All(pinPost)).Methods("POST")
apiColls.HandleFunc("/{alias}/inbox", handler.All(handleFetchCollectionInbox)).Methods("POST")
apiColls.HandleFunc("/{alias}/outbox", handler.AllReader(handleFetchCollectionOutbox)).Methods("GET")
apiColls.HandleFunc("/{alias}/following", handler.AllReader(handleFetchCollectionFollowing)).Methods("GET")
apiColls.HandleFunc("/{alias}/followers", handler.AllReader(handleFetchCollectionFollowers)).Methods("GET")
// Handle posts
write.HandleFunc("/api/posts", handler.All(newPost)).Methods("POST")
posts := write.PathPrefix("/api/posts/").Subrouter()
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.AllReader(fetchPost)).Methods("GET")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(existingPost)).Methods("POST", "PUT")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}", handler.All(deletePost)).Methods("DELETE")
posts.HandleFunc("/{post:[a-zA-Z0-9]{10}}/{property}", handler.AllReader(fetchPostProperty)).Methods("GET")
posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST")
posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST")
write.HandleFunc("/auth/signup", handler.Web(handleWebSignup, UserLevelNoneRequired)).Methods("POST")
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
+ write.HandleFunc("/admin/monitor", handler.Admin(handleViewAdminMonitor)).Methods("GET")
+ write.HandleFunc("/admin/settings", handler.Admin(handleViewAdminSettings)).Methods("GET")
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")
write.HandleFunc("/admin/page/{slug}", handler.Admin(handleViewAdminPage)).Methods("GET")
write.HandleFunc("/admin/update/config", handler.AdminApper(handleAdminUpdateConfig)).Methods("POST")
write.HandleFunc("/admin/update/{page}", handler.Admin(handleAdminUpdateSite)).Methods("POST")
// Handle special pages first
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired))
write.HandleFunc("/invite/{code:[a-zA-Z0-9]+}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET")
// TODO: show a reader-specific 404 page if the function is disabled
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader))
RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter())
draftEditPrefix := ""
if apper.App().cfg.App.SingleUser {
draftEditPrefix = "/d"
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
} else {
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
}
// All the existing stuff
write.HandleFunc(draftEditPrefix+"/{action}/edit", handler.Web(handleViewPad, UserLevelUser)).Methods("GET")
write.HandleFunc(draftEditPrefix+"/{action}/meta", handler.Web(handleViewMeta, UserLevelUser)).Methods("GET")
// Collections
if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader))
RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter())
// Posts
}
write.HandleFunc(draftEditPrefix+"/{post}", handler.Web(handleViewPost, UserLevelOptional))
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
return r
}
func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap))
r.HandleFunc("/feed/", handler.AllReader(ViewFeed))
r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET")
}
func RouteRead(handler *Handler, readPerm UserLevelFunc, r *mux.Router) {
r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm))
r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm))
r.HandleFunc("/t/{tag}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/a/{post}", handler.Web(handlePostIDRedirect, readPerm))
r.HandleFunc("/{author}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/", handler.Web(viewLocalTimeline, readPerm))
}
diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl
index 3cb81be..1a0e6b4 100644
--- a/templates/user/admin.tmpl
+++ b/templates/user/admin.tmpl
@@ -1,190 +1,68 @@
{{define "admin"}}
{{template "header" .}}
<style type="text/css">
h2 {font-weight: normal;}
ul.pagenav {list-style: none;}
form {
margin: 0 0 2em;
}
form dt {
line-height: inherit;
}
.ui.divider:not(.vertical):not(.horizontal) {
border-top: 1px solid rgba(34,36,38,.15);
border-bottom: 1px solid rgba(255,255,255,.1);
}
.ui.divider {
margin: 1rem 0;
line-height: 1;
height: 0;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
color: rgba(0,0,0,.85);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
font-size: 1rem;
}
.invisible {
display: none;
}
p.docs {
font-size: 0.86em;
}
+.stats {
+ font-size: 1.2em;
+ margin: 1em 0;
+}
+.num {
+ font-weight: bold;
+ font-size: 1.5em;
+}
</style>
<div class="content-container snug">
{{template "admin-header" .}}
{{if .Message}}<p>{{.Message}}</p>{{end}}
- <h2>On this page</h2>
- <ul class="pagenav">
- <li><a href="#config">Configuration</a></li>
- <li><a href="#monitor">Application monitor</a></li>
- </ul>
-
- <h2>Resources</h2>
- <ul class="pagenav">
- <li><a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin">Admin Guide</a></li>
- </ul>
-
- <hr />
-
- <h2><a name="config"></a>App Configuration</h2>
- <p class="docs">Read more in the <a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin/config">configuration docs</a>.</p>
-
- {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
-
- <form action="/admin/update/config" method="post">
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Name</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_name" id="site_name" class="inline" value="{{.Config.SiteName}}" style="width: 14em;" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Description</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_desc" id="site_desc" class="inline" value="{{.Config.SiteDesc}}" style="width: 14em;" /></dd>
- <dt>Host</dt>
- <dd>{{.Config.Host}}</dd>
- <dt>User Mode</dt>
- <dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
- <dt><label for="min_username_len">Minimum Username Length</label></dt>
- <dd><input type="number" name="min_username_len" id="min_username_len" class="inline" min="1" max="100" value="{{.Config.MinUsernameLen}}" /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="max_blogs">Maximum Blogs per User</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="number" name="max_blogs" id="max_blogs" class="inline" min="1" value="{{.Config.MaxBlogs}}" /></dd>
- <dt><label for="federation">Federation</label></dt>
- <dd><input type="checkbox" name="federation" id="federation" {{if .Config.Federation}}checked="checked"{{end}} /></dd>
- <dt><label for="public_stats">Public Stats</label></dt>
- <dd><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></dd>
- <dt><label for="private">Private Instance</label></dt>
- <dd><input type="checkbox" name="private" id="private" {{if .Config.Private}}checked="checked"{{end}} /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="local_timeline">Local Timeline</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="local_timeline" id="local_timeline" {{if .Config.LocalTimeline}}checked="checked"{{end}} /></dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites">Allow sending invitations by</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}>
- <select name="user_invites" id="user_invites">
- <option value="none" {{if eq .Config.UserInvites ""}}selected="selected"{{end}}>No one</option>
- <option value="user" {{if eq .Config.UserInvites "user"}}selected="selected"{{end}}>Users</option>
- <option value="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Admins</option>
- </select>
- </dd>
- <dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">Default blog visibility</label></dt>
- <dd{{if .Config.SingleUser}} class="invisible"{{end}}>
- <select name="default_visibility" id="default_visibility">
- <option value="unlisted" {{if eq .Config.DefaultVisibility "unlisted"}}selected="selected"{{end}}>Unlisted</option>
- <option value="public" {{if eq .Config.DefaultVisibility "public"}}selected="selected"{{end}}>Public</option>
- <option value="private" {{if eq .Config.DefaultVisibility "private"}}selected="selected"{{end}}>Private</option>
- </select>
- </dd>
- </dl>
- <input type="submit" value="Save Configuration" />
+ <div class="row stats">
+ <div><span class="num">{{largeNumFmt .UsersCount}}</span> {{pluralize "user" "users" .UsersCount}}</div>
+ <div><span class="num">{{largeNumFmt .CollectionsCount}}</span> {{pluralize "blog" "blogs" .CollectionsCount}}</div>
+ <div><span class="num">{{largeNumFmt .PostsCount}}</span> {{pluralize "post" "posts" .PostsCount}}</div>
</div>
- </form>
-
- <hr />
- <h2><a name="monitor"></a>Application</h2>
-
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt>WriteFreely</dt>
- <dd>{{.Version}}</dd>
- <dt>Server Uptime</dt>
- <dd>{{.SysStatus.Uptime}}</dd>
- <dt>Current Goroutines</dt>
- <dd>{{.SysStatus.NumGoroutine}}</dd>
- <div class="ui divider"></div>
- <dt>Current memory usage</dt>
- <dd>{{.SysStatus.MemAllocated}}</dd>
- <dt>Total mem allocated</dt>
- <dd>{{.SysStatus.MemTotal}}</dd>
- <dt>Memory obtained</dt>
- <dd>{{.SysStatus.MemSys}}</dd>
- <dt>Pointer lookup times</dt>
- <dd>{{.SysStatus.Lookups}}</dd>
- <dt>Memory allocate times</dt>
- <dd>{{.SysStatus.MemMallocs}}</dd>
- <dt>Memory free times</dt>
- <dd>{{.SysStatus.MemFrees}}</dd>
- <div class="ui divider"></div>
- <dt>Current heap usage</dt>
- <dd>{{.SysStatus.HeapAlloc}}</dd>
- <dt>Heap memory obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>Heap memory idle</dt>
- <dd>{{.SysStatus.HeapIdle}}</dd>
- <dt>Heap memory in use</dt>
- <dd>{{.SysStatus.HeapInuse}}</dd>
- <dt>Heap memory released</dt>
- <dd>{{.SysStatus.HeapReleased}}</dd>
- <dt>Heap objects</dt>
- <dd>{{.SysStatus.HeapObjects}}</dd>
- <div class="ui divider"></div>
- <dt>Bootstrap stack usage</dt>
- <dd>{{.SysStatus.StackInuse}}</dd>
- <dt>Stack memory obtained</dt>
- <dd>{{.SysStatus.StackSys}}</dd>
- <dt>MSpan structures in use</dt>
- <dd>{{.SysStatus.MSpanInuse}}</dd>
- <dt>MSpan structures obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>MCache structures in use</dt>
- <dd>{{.SysStatus.MCacheInuse}}</dd>
- <dt>MCache structures obtained</dt>
- <dd>{{.SysStatus.MCacheSys}}</dd>
- <dt>Profiling bucket hash table obtained</dt>
- <dd>{{.SysStatus.BuckHashSys}}</dd>
- <dt>GC metadata obtained</dt>
- <dd>{{.SysStatus.GCSys}}</dd>
- <dt>Other system allocation obtained</dt>
- <dd>{{.SysStatus.OtherSys}}</dd>
- <div class="ui divider"></div>
- <dt>Next GC recycle</dt>
- <dd>{{.SysStatus.NextGC}}</dd>
- <dt>Since last GC</dt>
- <dd>{{.SysStatus.LastGC}}</dd>
- <dt>Total GC pause</dt>
- <dd>{{.SysStatus.PauseTotalNs}}</dd>
- <dt>Last GC pause</dt>
- <dd>{{.SysStatus.PauseNs}}</dd>
- <dt>GC times</dt>
- <dd>{{.SysStatus.NumGC}}</dd>
- </dl>
- </div>
</div>
<script>
history.replaceState(null, "", "/admin"+window.location.hash);
</script>
{{template "footer" .}}
{{template "body-end" .}}
{{end}}
diff --git a/templates/user/admin.tmpl b/templates/user/admin/app-settings.tmpl
similarity index 57%
copy from templates/user/admin.tmpl
copy to templates/user/admin/app-settings.tmpl
index 3cb81be..4af6a44 100644
--- a/templates/user/admin.tmpl
+++ b/templates/user/admin/app-settings.tmpl
@@ -1,190 +1,85 @@
-{{define "admin"}}
+{{define "app-settings"}}
{{template "header" .}}
<style type="text/css">
h2 {font-weight: normal;}
-ul.pagenav {list-style: none;}
form {
margin: 0 0 2em;
}
form dt {
line-height: inherit;
}
-.ui.divider:not(.vertical):not(.horizontal) {
- border-top: 1px solid rgba(34,36,38,.15);
- border-bottom: 1px solid rgba(255,255,255,.1);
-}
-.ui.divider {
- margin: 1rem 0;
- line-height: 1;
- height: 0;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: .05em;
- color: rgba(0,0,0,.85);
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- -webkit-tap-highlight-color: transparent;
- font-size: 1rem;
-}
.invisible {
display: none;
}
p.docs {
font-size: 0.86em;
}
</style>
<div class="content-container snug">
{{template "admin-header" .}}
- {{if .Message}}<p>{{.Message}}</p>{{end}}
-
- <h2>On this page</h2>
- <ul class="pagenav">
- <li><a href="#config">Configuration</a></li>
- <li><a href="#monitor">Application monitor</a></li>
- </ul>
-
- <h2>Resources</h2>
- <ul class="pagenav">
- <li><a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin">Admin Guide</a></li>
- </ul>
+ {{if .Message}}<p><a name="config"></a>{{.Message}}</p>{{end}}
- <hr />
+ {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
- <h2><a name="config"></a>App Configuration</h2>
<p class="docs">Read more in the <a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin/config">configuration docs</a>.</p>
- {{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
-
<form action="/admin/update/config" method="post">
<div class="ui attached table segment">
<dl class="dl-horizontal admin-dl-horizontal">
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Name</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_name" id="site_name" class="inline" value="{{.Config.SiteName}}" style="width: 14em;" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Site Description</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_desc" id="site_desc" class="inline" value="{{.Config.SiteDesc}}" style="width: 14em;" /></dd>
<dt>Host</dt>
<dd>{{.Config.Host}}</dd>
<dt>User Mode</dt>
<dd>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}>Landing Page</dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">Open Registrations</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} /></dd>
<dt><label for="min_username_len">Minimum Username Length</label></dt>
<dd><input type="number" name="min_username_len" id="min_username_len" class="inline" min="1" max="100" value="{{.Config.MinUsernameLen}}" /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="max_blogs">Maximum Blogs per User</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="number" name="max_blogs" id="max_blogs" class="inline" min="1" value="{{.Config.MaxBlogs}}" /></dd>
<dt><label for="federation">Federation</label></dt>
<dd><input type="checkbox" name="federation" id="federation" {{if .Config.Federation}}checked="checked"{{end}} /></dd>
<dt><label for="public_stats">Public Stats</label></dt>
<dd><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></dd>
<dt><label for="private">Private Instance</label></dt>
<dd><input type="checkbox" name="private" id="private" {{if .Config.Private}}checked="checked"{{end}} /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="local_timeline">Local Timeline</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="local_timeline" id="local_timeline" {{if .Config.LocalTimeline}}checked="checked"{{end}} /></dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites">Allow sending invitations by</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}>
<select name="user_invites" id="user_invites">
<option value="none" {{if eq .Config.UserInvites ""}}selected="selected"{{end}}>No one</option>
<option value="user" {{if eq .Config.UserInvites "user"}}selected="selected"{{end}}>Users</option>
<option value="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Admins</option>
</select>
</dd>
<dt{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">Default blog visibility</label></dt>
<dd{{if .Config.SingleUser}} class="invisible"{{end}}>
<select name="default_visibility" id="default_visibility">
<option value="unlisted" {{if eq .Config.DefaultVisibility "unlisted"}}selected="selected"{{end}}>Unlisted</option>
<option value="public" {{if eq .Config.DefaultVisibility "public"}}selected="selected"{{end}}>Public</option>
<option value="private" {{if eq .Config.DefaultVisibility "private"}}selected="selected"{{end}}>Private</option>
</select>
</dd>
</dl>
- <input type="submit" value="Save Configuration" />
+ <input type="submit" value="Save Settings" />
</div>
</form>
-
- <hr />
-
- <h2><a name="monitor"></a>Application</h2>
-
- <div class="ui attached table segment">
- <dl class="dl-horizontal admin-dl-horizontal">
- <dt>WriteFreely</dt>
- <dd>{{.Version}}</dd>
- <dt>Server Uptime</dt>
- <dd>{{.SysStatus.Uptime}}</dd>
- <dt>Current Goroutines</dt>
- <dd>{{.SysStatus.NumGoroutine}}</dd>
- <div class="ui divider"></div>
- <dt>Current memory usage</dt>
- <dd>{{.SysStatus.MemAllocated}}</dd>
- <dt>Total mem allocated</dt>
- <dd>{{.SysStatus.MemTotal}}</dd>
- <dt>Memory obtained</dt>
- <dd>{{.SysStatus.MemSys}}</dd>
- <dt>Pointer lookup times</dt>
- <dd>{{.SysStatus.Lookups}}</dd>
- <dt>Memory allocate times</dt>
- <dd>{{.SysStatus.MemMallocs}}</dd>
- <dt>Memory free times</dt>
- <dd>{{.SysStatus.MemFrees}}</dd>
- <div class="ui divider"></div>
- <dt>Current heap usage</dt>
- <dd>{{.SysStatus.HeapAlloc}}</dd>
- <dt>Heap memory obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>Heap memory idle</dt>
- <dd>{{.SysStatus.HeapIdle}}</dd>
- <dt>Heap memory in use</dt>
- <dd>{{.SysStatus.HeapInuse}}</dd>
- <dt>Heap memory released</dt>
- <dd>{{.SysStatus.HeapReleased}}</dd>
- <dt>Heap objects</dt>
- <dd>{{.SysStatus.HeapObjects}}</dd>
- <div class="ui divider"></div>
- <dt>Bootstrap stack usage</dt>
- <dd>{{.SysStatus.StackInuse}}</dd>
- <dt>Stack memory obtained</dt>
- <dd>{{.SysStatus.StackSys}}</dd>
- <dt>MSpan structures in use</dt>
- <dd>{{.SysStatus.MSpanInuse}}</dd>
- <dt>MSpan structures obtained</dt>
- <dd>{{.SysStatus.HeapSys}}</dd>
- <dt>MCache structures in use</dt>
- <dd>{{.SysStatus.MCacheInuse}}</dd>
- <dt>MCache structures obtained</dt>
- <dd>{{.SysStatus.MCacheSys}}</dd>
- <dt>Profiling bucket hash table obtained</dt>
- <dd>{{.SysStatus.BuckHashSys}}</dd>
- <dt>GC metadata obtained</dt>
- <dd>{{.SysStatus.GCSys}}</dd>
- <dt>Other system allocation obtained</dt>
- <dd>{{.SysStatus.OtherSys}}</dd>
- <div class="ui divider"></div>
- <dt>Next GC recycle</dt>
- <dd>{{.SysStatus.NextGC}}</dd>
- <dt>Since last GC</dt>
- <dd>{{.SysStatus.LastGC}}</dd>
- <dt>Total GC pause</dt>
- <dd>{{.SysStatus.PauseTotalNs}}</dd>
- <dt>Last GC pause</dt>
- <dd>{{.SysStatus.PauseNs}}</dd>
- <dt>GC times</dt>
- <dd>{{.SysStatus.NumGC}}</dd>
- </dl>
- </div>
</div>
<script>
-history.replaceState(null, "", "/admin"+window.location.hash);
+history.replaceState(null, "", "/admin/settings"+window.location.hash);
</script>
{{template "footer" .}}
{{template "body-end" .}}
{{end}}
diff --git a/templates/user/admin/monitor.tmpl b/templates/user/admin/monitor.tmpl
new file mode 100644
index 0000000..e803dd3
--- /dev/null
+++ b/templates/user/admin/monitor.tmpl
@@ -0,0 +1,105 @@
+{{define "monitor"}}
+{{template "header" .}}
+
+<style type="text/css">
+h2 {font-weight: normal;}
+.ui.divider:not(.vertical):not(.horizontal) {
+ border-top: 1px solid rgba(34,36,38,.15);
+ border-bottom: 1px solid rgba(255,255,255,.1);
+}
+.ui.divider {
+ margin: 1rem 0;
+ line-height: 1;
+ height: 0;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ color: rgba(0,0,0,.85);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ font-size: 1rem;
+}
+</style>
+
+<div class="content-container snug">
+ {{template "admin-header" .}}
+
+ {{if .Message}}<p>{{.Message}}</p>{{end}}
+
+ <h2><a name="monitor"></a>Application Monitor</h2>
+
+ <div class="ui attached table segment">
+ <dl class="dl-horizontal admin-dl-horizontal">
+ <dt>WriteFreely</dt>
+ <dd>{{.Version}}</dd>
+ <dt>Server Uptime</dt>
+ <dd>{{.SysStatus.Uptime}}</dd>
+ <dt>Current Goroutines</dt>
+ <dd>{{.SysStatus.NumGoroutine}}</dd>
+ <div class="ui divider"></div>
+ <dt>Current memory usage</dt>
+ <dd>{{.SysStatus.MemAllocated}}</dd>
+ <dt>Total mem allocated</dt>
+ <dd>{{.SysStatus.MemTotal}}</dd>
+ <dt>Memory obtained</dt>
+ <dd>{{.SysStatus.MemSys}}</dd>
+ <dt>Pointer lookup times</dt>
+ <dd>{{.SysStatus.Lookups}}</dd>
+ <dt>Memory allocate times</dt>
+ <dd>{{.SysStatus.MemMallocs}}</dd>
+ <dt>Memory free times</dt>
+ <dd>{{.SysStatus.MemFrees}}</dd>
+ <div class="ui divider"></div>
+ <dt>Current heap usage</dt>
+ <dd>{{.SysStatus.HeapAlloc}}</dd>
+ <dt>Heap memory obtained</dt>
+ <dd>{{.SysStatus.HeapSys}}</dd>
+ <dt>Heap memory idle</dt>
+ <dd>{{.SysStatus.HeapIdle}}</dd>
+ <dt>Heap memory in use</dt>
+ <dd>{{.SysStatus.HeapInuse}}</dd>
+ <dt>Heap memory released</dt>
+ <dd>{{.SysStatus.HeapReleased}}</dd>
+ <dt>Heap objects</dt>
+ <dd>{{.SysStatus.HeapObjects}}</dd>
+ <div class="ui divider"></div>
+ <dt>Bootstrap stack usage</dt>
+ <dd>{{.SysStatus.StackInuse}}</dd>
+ <dt>Stack memory obtained</dt>
+ <dd>{{.SysStatus.StackSys}}</dd>
+ <dt>MSpan structures in use</dt>
+ <dd>{{.SysStatus.MSpanInuse}}</dd>
+ <dt>MSpan structures obtained</dt>
+ <dd>{{.SysStatus.HeapSys}}</dd>
+ <dt>MCache structures in use</dt>
+ <dd>{{.SysStatus.MCacheInuse}}</dd>
+ <dt>MCache structures obtained</dt>
+ <dd>{{.SysStatus.MCacheSys}}</dd>
+ <dt>Profiling bucket hash table obtained</dt>
+ <dd>{{.SysStatus.BuckHashSys}}</dd>
+ <dt>GC metadata obtained</dt>
+ <dd>{{.SysStatus.GCSys}}</dd>
+ <dt>Other system allocation obtained</dt>
+ <dd>{{.SysStatus.OtherSys}}</dd>
+ <div class="ui divider"></div>
+ <dt>Next GC recycle</dt>
+ <dd>{{.SysStatus.NextGC}}</dd>
+ <dt>Since last GC</dt>
+ <dd>{{.SysStatus.LastGC}}</dd>
+ <dt>Total GC pause</dt>
+ <dd>{{.SysStatus.PauseTotalNs}}</dd>
+ <dt>Last GC pause</dt>
+ <dd>{{.SysStatus.PauseNs}}</dd>
+ <dt>GC times</dt>
+ <dd>{{.SysStatus.NumGC}}</dd>
+ </dl>
+ </div>
+</div>
+
+{{template "footer" .}}
+
+{{template "body-end" .}}
+{{end}}
diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl
index 0704854..f7c06ef 100644
--- a/templates/user/include/header.tmpl
+++ b/templates/user/include/header.tmpl
@@ -1,108 +1,110 @@
{{define "user-navigation"}}
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}">
{{if .SingleUser}}
<nav id="user-nav">
<nav class="dropdown-nav">
<ul><li><a href="/" title="View blog" class="title">{{.SiteName}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
<ul>
<li><a href="/me/c/{{.Username}}">Customize</a></li>
<li><a href="/me/c/{{.Username}}/stats">Stats</a></li>
<li class="separator"><hr /></li>
{{if .IsAdmin}}<li><a href="/admin">Admin</a></li>{{end}}
<li><a href="/me/settings">Settings</a></li>
<li><a href="/me/import">Import posts</a></li>
<li><a href="/me/export">Export</a></li>
<li class="separator"><hr /></li>
<li><a href="/me/logout">Log out</a></li>
</ul></li>
</ul>
</nav>
<nav class="tabs">
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>
<a href="/me/new">New Post</a>
</nav>
</nav>
{{else}}
<nav id="full-nav">
<div class="left-side">
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1>
</div>
<nav id="user-nav">
{{if .Username}}
<nav class="dropdown-nav">
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul>
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}}
<li><a href="/me/settings">Account settings</a></li>
<li><a href="/me/import">Import posts</a></li>
<li><a href="/me/export">Export</a></li>
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}}
<li class="separator"><hr /></li>
<li><a href="/me/logout">Log out</a></li>
</ul></li>
</ul>
</nav>
{{end}}
<nav class="tabs">
{{if .SimpleNav}}
{{ if not .SingleUser }}
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}}
{{ end }}
<a href="/about">About</a>
{{ if not .SingleUser }}
{{ if .Username }}
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}}
{{if and .Chorus (eq .MaxBlogs 1)}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}}
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
{{ end }}
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}}
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}}
{{ end }}
{{else}}
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}}
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}}
{{end}}
</nav>
</nav>
{{if .Chorus}}{{if .Username}}<div class="right-side">
<a class="simple-btn" href="/new">New Post</a>
</div>{{end}}
</nav>
{{end}}
{{end}}
</header>
{{end}}
{{define "header"}}<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}&mdash;{{end}} {{.SiteName}}</title>
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#888888" />
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}">
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png">
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png">
</head>
<body id="me">
{{template "user-navigation" .}}
<div id="official-writing">
{{end}}
{{define "admin-header"}}
<header class="admin">
<h1>Admin</h1>
- <nav id="admin">
+ <nav id="admin" class="pager">
<a href="/admin" {{if eq .Path "/admin"}}class="selected"{{end}}>Dashboard</a>
+ <a href="/admin/settings" {{if eq .Path "/admin/settings"}}class="selected"{{end}}>Settings</a>
{{if not .SingleUser}}
<a href="/admin/users" {{if eq .Path "/admin/users"}}class="selected"{{end}}>Users</a>
<a href="/admin/pages" {{if eq .Path "/admin/pages"}}class="selected"{{end}}>Pages</a>
{{end}}
+ <a href="/admin/monitor" {{if eq .Path "/admin/monitor"}}class="selected"{{end}}>Monitor</a>
</nav>
</header>
{{end}}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Feb 1, 1:41 AM (20 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3146019

Event Timeline