Merge pull request #264 from writeas/admin-dashboard-redesign
Admin dashboard redesign Closes T694
This commit is contained in:
commit
4595d480ae
89
admin.go
89
admin.go
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
* Copyright © 2018-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -90,6 +90,18 @@ type instanceContent struct {
|
||||||
Updated time.Time
|
Updated time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminPage struct {
|
||||||
|
UpdateAvailable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminPage(app *App) *AdminPage {
|
||||||
|
ap := &AdminPage{}
|
||||||
|
if app.updates != nil {
|
||||||
|
ap.UpdateAvailable = app.updates.AreAvailableNoCheck()
|
||||||
|
}
|
||||||
|
return ap
|
||||||
|
}
|
||||||
|
|
||||||
func (c instanceContent) UpdatedFriendly() string {
|
func (c instanceContent) UpdatedFriendly() string {
|
||||||
/*
|
/*
|
||||||
// TODO: accept a locale in this method and use that for the format
|
// TODO: accept a locale in this method and use that for the format
|
||||||
|
@ -100,28 +112,81 @@ func (c instanceContent) UpdatedFriendly() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
|
p := struct {
|
||||||
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
|
Message string
|
||||||
|
|
||||||
|
UsersCount, CollectionsCount, PostsCount int64
|
||||||
|
}{
|
||||||
|
UserPage: NewUserPage(app, r, u, "Admin", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
|
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()
|
updateAppStats()
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
SysStatus systemStatus
|
SysStatus systemStatus
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
|
|
||||||
Message, ConfigMessage string
|
Message, ConfigMessage string
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Admin", nil),
|
UserPage: NewUserPage(app, r, u, "Admin", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
SysStatus: sysStatus,
|
SysStatus: sysStatus,
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
|
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
ConfigMessage: r.FormValue("cm"),
|
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
|
||||||
|
*AdminPage
|
||||||
|
Config config.AppCfg
|
||||||
|
|
||||||
|
Message, ConfigMessage string
|
||||||
|
}{
|
||||||
|
UserPage: NewUserPage(app, r, u, "Admin", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
|
Config: app.cfg.App,
|
||||||
|
|
||||||
|
Message: r.FormValue("m"),
|
||||||
|
ConfigMessage: r.FormValue("cm"),
|
||||||
|
}
|
||||||
|
|
||||||
|
showUserPage(w, "app-settings", p)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
|
@ -131,6 +196,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
||||||
TotalPages []int
|
TotalPages []int
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Users", nil),
|
UserPage: NewUserPage(app, r, u, "Users", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
}
|
}
|
||||||
|
@ -168,6 +234,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
|
@ -178,6 +245,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
TotalPosts int64
|
TotalPosts int64
|
||||||
ClearEmail string
|
ClearEmail string
|
||||||
}{
|
}{
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
Colls: []inspectedCollection{},
|
Colls: []inspectedCollection{},
|
||||||
|
@ -303,12 +371,14 @@ func handleAdminResetUserPass(app *App, u *User, w http.ResponseWriter, r *http.
|
||||||
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
Pages []*instanceContent
|
Pages []*instanceContent
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Pages", nil),
|
UserPage: NewUserPage(app, r, u, "Pages", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
}
|
}
|
||||||
|
@ -367,12 +437,14 @@ func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
Config config.AppCfg
|
Config config.AppCfg
|
||||||
Message string
|
Message string
|
||||||
|
|
||||||
Banner *instanceContent
|
Banner *instanceContent
|
||||||
Content *instanceContent
|
Content *instanceContent
|
||||||
}{
|
}{
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
Config: app.cfg.App,
|
Config: app.cfg.App,
|
||||||
Message: r.FormValue("m"),
|
Message: r.FormValue("m"),
|
||||||
}
|
}
|
||||||
|
@ -474,7 +546,7 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m = "?cm=" + err.Error()
|
m = "?cm=" + err.Error()
|
||||||
}
|
}
|
||||||
return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
|
return impart.HTTPError{http.StatusFound, "/admin/settings" + m + "#config"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAppStats() {
|
func updateAppStats() {
|
||||||
|
@ -537,18 +609,27 @@ func handleViewAdminUpdates(app *App, u *User, w http.ResponseWriter, r *http.Re
|
||||||
|
|
||||||
p := struct {
|
p := struct {
|
||||||
*UserPage
|
*UserPage
|
||||||
|
*AdminPage
|
||||||
|
CurReleaseNotesURL string
|
||||||
LastChecked string
|
LastChecked string
|
||||||
|
LastChecked8601 string
|
||||||
LatestVersion string
|
LatestVersion string
|
||||||
LatestReleaseURL string
|
LatestReleaseURL string
|
||||||
UpdateAvailable bool
|
LatestReleaseNotesURL string
|
||||||
|
CheckFailed bool
|
||||||
}{
|
}{
|
||||||
UserPage: NewUserPage(app, r, u, "Updates", nil),
|
UserPage: NewUserPage(app, r, u, "Updates", nil),
|
||||||
|
AdminPage: NewAdminPage(app),
|
||||||
}
|
}
|
||||||
|
p.CurReleaseNotesURL = wfReleaseNotesURL(p.Version)
|
||||||
if app.cfg.App.UpdateChecks {
|
if app.cfg.App.UpdateChecks {
|
||||||
p.LastChecked = app.updates.lastCheck.Format("January 2, 2006, 3:04 PM")
|
p.LastChecked = app.updates.lastCheck.Format("January 2, 2006, 3:04 PM")
|
||||||
|
p.LastChecked8601 = app.updates.lastCheck.Format("2006-01-02T15:04:05Z")
|
||||||
p.LatestVersion = app.updates.LatestVersion()
|
p.LatestVersion = app.updates.LatestVersion()
|
||||||
p.LatestReleaseURL = app.updates.ReleaseURL()
|
p.LatestReleaseURL = app.updates.ReleaseURL()
|
||||||
|
p.LatestReleaseNotesURL = app.updates.ReleaseNotesURL()
|
||||||
p.UpdateAvailable = app.updates.AreAvailable()
|
p.UpdateAvailable = app.updates.AreAvailable()
|
||||||
|
p.CheckFailed = app.updates.checkError != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
showUserPage(w, "app-updates", p)
|
showUserPage(w, "app-updates", p)
|
||||||
|
|
|
@ -94,6 +94,7 @@ type (
|
||||||
|
|
||||||
// Site functionality
|
// Site functionality
|
||||||
Chorus bool `ini:"chorus"`
|
Chorus bool `ini:"chorus"`
|
||||||
|
Forest bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info.
|
||||||
DisableDrafts bool `ini:"disable_drafts"`
|
DisableDrafts bool `ini:"disable_drafts"`
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
|
|
|
@ -13,15 +13,21 @@ nav#admin {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
a {
|
a {
|
||||||
color: @primary;
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
.rounded(.25em);
|
||||||
|
border: 0;
|
||||||
&.selected {
|
&.selected {
|
||||||
|
background: #dedede;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
.blip {
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.blip {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
.pager {
|
.pager {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -42,3 +48,39 @@ nav#admin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
.btn {
|
||||||
|
font-family: @sansFont;
|
||||||
|
font-size: 0.86em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
margin: 1em 0;
|
||||||
|
|
||||||
|
div {
|
||||||
|
&:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
&+div {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 0.86em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
div.row.features {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.features div + div {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -865,20 +865,6 @@ input {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div.features {
|
|
||||||
margin-top: 1.5em;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.86em;
|
|
||||||
ul {
|
|
||||||
text-align: left;
|
|
||||||
max-width: 26em;
|
|
||||||
margin-left: auto !important;
|
|
||||||
margin-right: auto !important;
|
|
||||||
li.soon, span.soon {
|
|
||||||
color: lighten(#111, 40%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div.blurbs {
|
div.blurbs {
|
||||||
>h2 {
|
>h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1342,6 +1328,16 @@ div.row {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.check, .blip {
|
||||||
|
font-size: 1.125em;
|
||||||
|
color: #71D571;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex.failure {
|
||||||
|
font-weight: bold;
|
||||||
|
color: @dangerCol;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 450px) {
|
@media all and (max-width: 450px) {
|
||||||
body#post {
|
body#post {
|
||||||
header {
|
header {
|
||||||
|
@ -1408,7 +1404,7 @@ div.row {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 600px) {
|
@media all and (max-width: 600px) {
|
||||||
div.row {
|
div.row:not(.admin-actions) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.half {
|
.half {
|
||||||
|
|
|
@ -152,6 +152,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
|
write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST")
|
||||||
|
|
||||||
write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET")
|
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/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
|
||||||
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).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}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
|
||||||
|
|
|
@ -35,6 +35,14 @@ form dt {
|
||||||
p.docs {
|
p.docs {
|
||||||
font-size: 0.86em;
|
font-size: 0.86em;
|
||||||
}
|
}
|
||||||
|
.stats {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="content-container snug">
|
<div class="content-container snug">
|
||||||
|
@ -42,142 +50,12 @@ p.docs {
|
||||||
|
|
||||||
{{if .Message}}<p>{{.Message}}</p>{{end}}
|
{{if .Message}}<p>{{.Message}}</p>{{end}}
|
||||||
|
|
||||||
<h2>On this page</h2>
|
<div class="row stats">
|
||||||
<ul class="pagenav">
|
<div><span class="num">{{largeNumFmt .UsersCount}}</span> {{pluralize "user" "users" .UsersCount}}</div>
|
||||||
<li><a href="#config">Configuration</a></li>
|
<div><span class="num">{{largeNumFmt .CollectionsCount}}</span> {{pluralize "blog" "blogs" .CollectionsCount}}</div>
|
||||||
<li><a href="#monitor">Application monitor</a></li>
|
<div><span class="num">{{largeNumFmt .PostsCount}}</span> {{pluralize "post" "posts" .PostsCount}}</div>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
{{define "app-settings"}}
|
||||||
|
{{template "header" .}}
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
h2 {font-weight: normal;}
|
||||||
|
form {
|
||||||
|
margin: 0 0 2em;
|
||||||
|
}
|
||||||
|
form dt {
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
.invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
p.docs {
|
||||||
|
font-size: 0.86em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="content-container snug">
|
||||||
|
{{template "admin-header" .}}
|
||||||
|
|
||||||
|
{{if .Message}}<p><a name="config"></a>{{.Message}}</p>{{end}}
|
||||||
|
|
||||||
|
{{if .ConfigMessage}}<p class="success" style="text-align: center">{{.ConfigMessage}}</p>{{end}}
|
||||||
|
|
||||||
|
<form action="/admin/update/config" method="post">
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}>
|
||||||
|
Site Title
|
||||||
|
<p>Your public site name.</p>
|
||||||
|
</div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_name" id="site_name" class="inline" value="{{.Config.SiteName}}" style="width: 14em;"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}>
|
||||||
|
Site Description
|
||||||
|
<p>Describe your site — this shows in your site's metadata.</p>
|
||||||
|
</div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="site_desc" id="site_desc" class="inline" value="{{.Config.SiteDesc}}" style="width: 14em;"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div>
|
||||||
|
Host
|
||||||
|
<p>The address where your site lives.</p>
|
||||||
|
</div>
|
||||||
|
<div>{{.Config.Host}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div>
|
||||||
|
Community Mode
|
||||||
|
<p>Whether your site is made for one person or many.</p>
|
||||||
|
</div>
|
||||||
|
<div>{{if .Config.SingleUser}}Single user{{else}}Multiple users{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}>
|
||||||
|
Landing Page
|
||||||
|
<p>The page that logged-out visitors will see first. This should be a path, e.g. <code>/read</code></p>
|
||||||
|
</div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="text" name="landing" id="landing" class="inline" value="{{.Config.Landing}}" style="width: 14em;"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="open_registration">
|
||||||
|
Open Registrations
|
||||||
|
<p>Whether or not registration is open to anyone who visits the site.</p>
|
||||||
|
</label></div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="open_registration" id="open_registration" {{if .Config.OpenRegistration}}checked="checked"{{end}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div><label for="min_username_len">
|
||||||
|
Minimum Username Length
|
||||||
|
<p>The minimum number of characters allowed in a username. (Recommended: 2 or more.)</p>
|
||||||
|
</label></div>
|
||||||
|
<div><input type="number" name="min_username_len" id="min_username_len" class="inline" min="1" max="100" value="{{.Config.MinUsernameLen}}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="max_blogs">
|
||||||
|
Maximum Blogs per User
|
||||||
|
<p>Keep things simple by setting this to <strong>1</strong>, unlimited by setting to <strong>0</strong>, or pick another amount.</p>
|
||||||
|
</label></div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="number" name="max_blogs" id="max_blogs" class="inline" min="0" value="{{.Config.MaxBlogs}}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div><label for="federation">
|
||||||
|
Federation
|
||||||
|
<p>Enable accounts on this site to propagate their posts via the ActivityPub protocol.</p>
|
||||||
|
</label></div>
|
||||||
|
<div><input type="checkbox" name="federation" id="federation" {{if .Config.Federation}}checked="checked"{{end}} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div><label for="public_stats">
|
||||||
|
Public Stats
|
||||||
|
<p>Publicly display the number of users and posts on your <strong>About</strong> page.</p>
|
||||||
|
</label></div>
|
||||||
|
<div><input type="checkbox" name="public_stats" id="public_stats" {{if .Config.PublicStats}}checked="checked"{{end}} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div><label for="private">
|
||||||
|
Private Instance
|
||||||
|
<p>Make this instance accessible only to those with an account.</p>
|
||||||
|
</label></div>
|
||||||
|
<div><input type="checkbox" name="private" id="private" {{if .Config.Private}}checked="checked"{{end}} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="local_timeline">
|
||||||
|
Reader
|
||||||
|
<p>Show a feed of user posts for anyone who chooses to share there.</p>
|
||||||
|
</label></div>
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><input type="checkbox" name="local_timeline" id="local_timeline" {{if .Config.LocalTimeline}}checked="checked"{{end}} /></div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="user_invites">
|
||||||
|
Allow invitations from...
|
||||||
|
<p>Choose who on this instance can invite new people.</p>
|
||||||
|
</label></div>
|
||||||
|
<div{{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="admin" {{if eq .Config.UserInvites "admin"}}selected="selected"{{end}}>Only Admins</option>
|
||||||
|
<option value="user" {{if eq .Config.UserInvites "user"}}selected="selected"{{end}}>All Users</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<div{{if .Config.SingleUser}} class="invisible"{{end}}><label for="default_visibility">
|
||||||
|
Default blog visibility
|
||||||
|
<p>The default setting for new accounts and blogs.</p>
|
||||||
|
</label></div>
|
||||||
|
<div{{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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="features row">
|
||||||
|
<input type="submit" value="Save Settings" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="docs">Still have questions? Read more details in the <a href="https://writefreely.org/docs/{{.OfficialVersion}}/admin/config">configuration docs</a>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
history.replaceState(null, "", "/admin/settings"+window.location.hash);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{template "footer" .}}
|
||||||
|
|
||||||
|
{{template "body-end" .}}
|
||||||
|
{{end}}
|
|
@ -2,20 +2,45 @@
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
p.intro {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
p.disabled {
|
||||||
|
font-style: italic;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="content-container snug">
|
<div class="content-container snug">
|
||||||
{{template "admin-header" .}}
|
{{template "admin-header" .}}
|
||||||
|
|
||||||
{{if not .UpdateAvailable}}
|
{{ if .UpdateChecks }}
|
||||||
<p class="alert info">WriteFreely is up to date.</p>
|
{{if .CheckFailed}}
|
||||||
|
<p class="intro"><span class="ex failure">×</span> Automated update check failed.</p>
|
||||||
|
<p>Installed version: <strong>{{.Version}}</strong> (<a href="{{.CurReleaseNotesURL}}" target="changelog-wf">release notes</a>).</p>
|
||||||
|
<p>Learn about latest releases on the <a href="https://blog.writefreely.org/tag:release" target="changelog-wf">WriteFreely blog</a> or <a href="https://discuss.write.as/c/writefreely/updates" target="forum-wf">forum</a>.</p>
|
||||||
|
{{else if not .UpdateAvailable}}
|
||||||
|
<p class="intro"><span class="check">✓</span> WriteFreely is <strong>up to date</strong>.</p>
|
||||||
|
<p>Installed version: <strong>{{.Version}}</strong> (<a href="{{.LatestReleaseNotesURL}}" target="changelog-wf">release notes</a>).</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="alert info">WriteFreely {{.LatestVersion}} is available.</p>
|
<p class="intro">A new version of WriteFreely is available! <a href="{{.LatestReleaseURL}}" target="download-wf" style="font-weight: bold;">Get {{.LatestVersion}}</a></p>
|
||||||
<section class="changelog">
|
<p class="changelog">
|
||||||
For details on features, bug fixes or notes on upgrading, <a href="{{.LatestReleaseURL}}">read the release notes</a>.
|
<a href="{{.LatestReleaseNotesURL}}" target="changelog-wf">Read the release notes</a> for details on features, bug fixes, and notes on upgrading from your current version, <strong>{{.Version}}</strong>.
|
||||||
</section>
|
</p>
|
||||||
|
{{end}}
|
||||||
|
<p style="font-size: 0.86em;"><em>Last checked</em>: <time class="dt-published" datetime="{{.LastChecked8601}}">{{.LastChecked}}</time>. <a href="/admin/updates?check=now">Check now</a>.</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Code modified from /js/localdate.js
|
||||||
|
var displayEl = document.querySelector("time");
|
||||||
|
var d = new Date(displayEl.getAttribute("datetime"));
|
||||||
|
displayEl.textContent = d.toLocaleDateString(navigator.language || "en-US", { dateStyle: 'long', timeStyle: 'short' });
|
||||||
|
</script>
|
||||||
|
{{ else }}
|
||||||
|
<p class="intro disabled">Automated update checks are disabled.</p>
|
||||||
|
<p>Installed version: <strong>{{.Version}}</strong> (<a href="{{.CurReleaseNotesURL}}" target="changelog-wf">release notes</a>).</p>
|
||||||
|
<p>Learn about latest releases on the <a href="https://blog.writefreely.org/tag:release" target="changelog-wf">WriteFreely blog</a> or <a href="https://discuss.write.as/c/writefreely/updates" target="forum-wf">forum</a>.</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<p>Last checked at: {{.LastChecked}}. <a href="/admin/updates?check=now">Check now</a>.</p>
|
|
||||||
|
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
|
|
||||||
|
|
|
@ -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}}
|
|
@ -4,7 +4,10 @@
|
||||||
<div class="snug content-container">
|
<div class="snug content-container">
|
||||||
{{template "admin-header" .}}
|
{{template "admin-header" .}}
|
||||||
|
|
||||||
<h2 id="posts-header" style="display: flex; justify-content: space-between;">Users <span style="font-style: italic; font-size: 0.75em;">{{.TotalUsers}} total</strong></h2>
|
<div class="row admin-actions" style="justify-content: space-between;">
|
||||||
|
<span style="font-style: italic; font-size: 1.2em">{{.TotalUsers}} {{pluralize "user" "users" .TotalUsers}}</span>
|
||||||
|
<a class="btn cta" href="/me/invites">+ Invite people</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="classy export" style="width:100%">
|
<table class="classy export" style="width:100%">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -71,8 +71,7 @@ input.copy-text {
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}>
|
<form action="/admin/user/{{.User.Username}}/status" method="POST" {{if not .User.IsSilenced}}onsubmit="return confirmSilence()"{{end}}>
|
||||||
<a id="status"/>
|
<th><a id="status"></a>Status</th>
|
||||||
<th>Status</th>
|
|
||||||
<td class="active-silence">
|
<td class="active-silence">
|
||||||
{{if .User.IsSilenced}}
|
{{if .User.IsSilenced}}
|
||||||
<p>Silenced</p>
|
<p>Silenced</p>
|
||||||
|
|
|
@ -97,12 +97,16 @@
|
||||||
{{define "admin-header"}}
|
{{define "admin-header"}}
|
||||||
<header class="admin">
|
<header class="admin">
|
||||||
<h1>Admin</h1>
|
<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" {{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}}
|
{{if not .SingleUser}}
|
||||||
<a href="/admin/users" {{if eq .Path "/admin/users"}}class="selected"{{end}}>Users</a>
|
<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>
|
<a href="/admin/pages" {{if eq .Path "/admin/pages"}}class="selected"{{end}}>Pages</a>
|
||||||
{{if .UpdateChecks}}<a href="/admin/updates" {{if eq .Path "/admin/updates"}}class="selected"{{end}}>Updates</a>{{end}}
|
{{if .UpdateChecks}}<a href="/admin/updates" {{if eq .Path "/admin/updates"}}class="selected"{{end}}>Updates{{if .UpdateAvailable}}<span class="blip">!</span>{{end}}</a>{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if not .Forest}}
|
||||||
|
<a href="/admin/monitor" {{if eq .Path "/admin/monitor"}}class="selected"{{end}}>Monitor</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
37
updates.go
37
updates.go
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2018-2019 A Bunch Tell LLC.
|
* Copyright © 2019-2020 A Bunch Tell LLC.
|
||||||
*
|
*
|
||||||
* This file is part of WriteFreely.
|
* This file is part of WriteFreely.
|
||||||
*
|
*
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
package writefreely
|
package writefreely
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/writeas/web-core/log"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -30,19 +31,25 @@ type updatesCache struct {
|
||||||
lastCheck time.Time
|
lastCheck time.Time
|
||||||
latestVersion string
|
latestVersion string
|
||||||
currentVersion string
|
currentVersion string
|
||||||
|
checkError error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckNow asks for the latest released version of writefreely and updates
|
// CheckNow asks for the latest released version of writefreely and updates
|
||||||
// the cache last checked time. If the version postdates the current 'latest'
|
// the cache last checked time. If the version postdates the current 'latest'
|
||||||
// the version value is replaced.
|
// the version value is replaced.
|
||||||
func (uc *updatesCache) CheckNow() error {
|
func (uc *updatesCache) CheckNow() error {
|
||||||
|
if debugging {
|
||||||
|
log.Info("[update check] Checking for update now.")
|
||||||
|
}
|
||||||
uc.mu.Lock()
|
uc.mu.Lock()
|
||||||
defer uc.mu.Unlock()
|
defer uc.mu.Unlock()
|
||||||
|
uc.lastCheck = time.Now()
|
||||||
latestRemote, err := newVersionCheck()
|
latestRemote, err := newVersionCheck()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("[update check] Failed: %v", err)
|
||||||
|
uc.checkError = err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uc.lastCheck = time.Now()
|
|
||||||
if CompareSemver(latestRemote, uc.latestVersion) == 1 {
|
if CompareSemver(latestRemote, uc.latestVersion) == 1 {
|
||||||
uc.latestVersion = latestRemote
|
uc.latestVersion = latestRemote
|
||||||
}
|
}
|
||||||
|
@ -58,15 +65,29 @@ func (uc updatesCache) AreAvailable() bool {
|
||||||
return CompareSemver(uc.latestVersion, uc.currentVersion) == 1
|
return CompareSemver(uc.latestVersion, uc.currentVersion) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AreAvailableNoCheck returns if the latest release is newer than the current
|
||||||
|
// running version.
|
||||||
|
func (uc updatesCache) AreAvailableNoCheck() bool {
|
||||||
|
return CompareSemver(uc.latestVersion, uc.currentVersion) == 1
|
||||||
|
}
|
||||||
|
|
||||||
// LatestVersion returns the latest stored version available.
|
// LatestVersion returns the latest stored version available.
|
||||||
func (uc updatesCache) LatestVersion() string {
|
func (uc updatesCache) LatestVersion() string {
|
||||||
return uc.latestVersion
|
return uc.latestVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseURL returns the full URL to the blog.writefreely.org release notes
|
|
||||||
// for the latest version as stored in the cache.
|
|
||||||
func (uc updatesCache) ReleaseURL() string {
|
func (uc updatesCache) ReleaseURL() string {
|
||||||
ver := strings.TrimPrefix(uc.latestVersion, "v")
|
return "https://writefreely.org/releases/" + uc.latestVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseNotesURL returns the full URL to the blog.writefreely.org release notes
|
||||||
|
// for the latest version as stored in the cache.
|
||||||
|
func (uc updatesCache) ReleaseNotesURL() string {
|
||||||
|
return wfReleaseNotesURL(uc.latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wfReleaseNotesURL(v string) string {
|
||||||
|
ver := strings.TrimPrefix(v, "v")
|
||||||
ver = strings.TrimSuffix(ver, ".0")
|
ver = strings.TrimSuffix(ver, ".0")
|
||||||
// hack until go 1.12 in build/travis
|
// hack until go 1.12 in build/travis
|
||||||
seg := strings.Split(ver, ".")
|
seg := strings.Split(ver, ".")
|
||||||
|
@ -79,7 +100,7 @@ func newUpdatesCache(expiry time.Duration) *updatesCache {
|
||||||
frequency: expiry,
|
frequency: expiry,
|
||||||
currentVersion: "v" + softwareVer,
|
currentVersion: "v" + softwareVer,
|
||||||
}
|
}
|
||||||
cache.CheckNow()
|
go cache.CheckNow()
|
||||||
return &cache
|
return &cache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +114,10 @@ func (app *App) InitUpdates() {
|
||||||
|
|
||||||
func newVersionCheck() (string, error) {
|
func newVersionCheck() (string, error) {
|
||||||
res, err := http.Get("https://version.writefreely.org")
|
res, err := http.Get("https://version.writefreely.org")
|
||||||
|
if debugging {
|
||||||
|
log.Info("[update check] GET https://version.writefreely.org")
|
||||||
|
}
|
||||||
|
// TODO: return error if statusCode != OK
|
||||||
if err == nil && res.StatusCode == http.StatusOK {
|
if err == nil && res.StatusCode == http.StatusOK {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ func TestUpdatesRoundTrip(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Release URL", func(t *testing.T) {
|
t.Run("Release URL", func(t *testing.T) {
|
||||||
url := cache.ReleaseURL()
|
url := cache.ReleaseNotesURL()
|
||||||
|
|
||||||
reg, err := regexp.Compile(`^https:\/\/blog.writefreely.org\/version(-\d+){1,}$`)
|
reg, err := regexp.Compile(`^https:\/\/blog.writefreely.org\/version(-\d+){1,}$`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue