chore(release): stabilize UI and validate critical flows #37

Merged
tony merged 2 commits from codex/block-10-stabilization into main 2026-08-14 16:31:26 +02:00
12 changed files with 54 additions and 8 deletions
+1
View File
@@ -32,6 +32,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
- SSRF-resistant HTTPS webhook delivery with redirect/address revalidation, event IDs, timestamps and optional HMAC-SHA256 signatures.
- Compact allow-listed audit events for authentication and significant mutations, administrator filtering, bounded manual purge, daily retention and maximum-count enforcement.
- Responsive server-rendered application shell with synthwave-derived design tokens, permission-aware navigation, the official DoGaMa wordmark in the sidebar, searchable real instance cards, lifecycle summaries, server-only recent Audit activity and resilient agent/database/storage/backup/audit status on the Dashboard.
- The responsive shell exposes a keyboard-accessible mobile navigation toggle on every authenticated SSR page; all recent-administrator form mutations return an explicit CSRF denial when request verification fails.
- Dedicated administrator Audit and Settings pages; notification channels, audit retention/purge and game-container labels retain their existing backend contracts outside the Dashboard.
- Audit uses server-side filtering and 50-event pagination; timestamps remain UTC in SQLite and are rendered in the Compose `TZ` IANA timezone with an invalid-zone fallback to UTC.
- Separate personal account settings and administrator user management, including email/password preferences, active-state session revocation, protected global roles, per-instance memberships and permission overrides.
+5
View File
@@ -26,6 +26,11 @@ func (s *server) requireRecentAdmin(w http.ResponseWriter, r *http.Request, api
}
}
if !ok {
if api {
s.apiProblem(w, http.StatusForbidden, "csrf_failed", "Request verification failed.")
} else {
s.problem(w, http.StatusForbidden, message("error.csrf"))
}
return auth.User{}, false
}
if time.Since(user.AuthenticatedAt) > 10*time.Minute {
+13
View File
@@ -331,6 +331,7 @@ func newHandlerServicesWithCatalog(authService *auth.Service, repository reposit
}
mux.HandleFunc("GET /static/app.v1.css", s.stylesheet)
mux.HandleFunc("GET /static/app.v2.css", s.stylesheet)
mux.HandleFunc("GET /static/mobile-fixes.css", s.mobileStylesheet)
mux.HandleFunc("GET /static/theme.v1.css", s.themeStylesheet)
mux.HandleFunc("GET /static/theme.v2.css", s.themeStylesheet)
mux.HandleFunc("GET /static/app.v1.js", s.javascript)
@@ -2214,6 +2215,18 @@ func (s *server) stylesheet(w http.ResponseWriter, _ *http.Request) {
_, _ = io.Copy(w, body)
}
func (s *server) mobileStylesheet(w http.ResponseWriter, _ *http.Request) {
body, err := assets.Open("static/mobile-fixes.css")
if err != nil {
http.Error(w, "Not found.", http.StatusNotFound)
return
}
defer func() { _ = body.Close() }()
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=86400")
_, _ = io.Copy(w, body)
}
func (s *server) themeStylesheet(w http.ResponseWriter, _ *http.Request) {
body, err := assets.Open("static/theme.v1.css")
if err != nil {
+1 -1
View File
@@ -477,7 +477,7 @@ func TestApplicationNavigationHidesAdministrationFromRegularUsers(t *testing.T)
cookies := []*http.Cookie{{Name: sessionCookie, Value: session.Token}, {Name: csrfCookie, Value: session.CSRFToken}}
home := request(t, handler, http.MethodGet, "/", cookies)
assertStatus(t, home, http.StatusOK)
if strings.Contains(home.Body.String(), "nav-instances") || strings.Contains(home.Body.String(), "nav-backups") || !strings.Contains(home.Body.String(), "href=\"/account\"") {
if strings.Contains(home.Body.String(), "nav-instances") || strings.Contains(home.Body.String(), "nav-backups") || !strings.Contains(home.Body.String(), "href=\"/account\"") || !strings.Contains(home.Body.String(), "class=\"mobile-header\"") || !strings.Contains(home.Body.String(), "aria-controls=\"app-navigation\"") {
t.Fatal("account navigation or removed global navigation is incorrect")
}
if strings.Contains(home.Body.String(), "href=\"/audit\"") || strings.Contains(home.Body.String(), "href=\"/administration\"") {
+2 -1
View File
@@ -1,4 +1,5 @@
document.querySelectorAll(".menu-button").forEach((button) => {
document.querySelectorAll(".menu-button:not([data-menu-bound])").forEach((button) => {
button.dataset.menuBound = "true";
button.addEventListener("click", () => {
const open = document.body.classList.toggle("nav-open");
button.setAttribute("aria-expanded", String(open));
+5
View File
@@ -0,0 +1,5 @@
@media (max-width: 760px) {
html, body { overflow-x: hidden; }
.mobile-header { width: 100vw; margin-left: calc(50% - 50vw); margin-right: 0; }
.tabs { flex-wrap: wrap; overflow-x: visible; }
}
+1 -1
View File
@@ -1 +1 @@
{{define "account.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Settings · DoGaMa</title><link rel="stylesheet" href="/static/app.v1.css"></head><body class="app-body">{{template "sidebar" .}}<main class="app-main"><div class="page-heading"><h1>Settings</h1><p>Personal account preferences.</p></div><nav class="tabs"><a href="#profile">Profile</a><a href="#security">Security</a><a href="#language">Language</a><a href="#notifications">Notifications</a></nav><section class="panel settings-section" id="profile"><h2>Profile</h2><p>Signed in as <strong>{{.User.Username}}</strong>.</p><form method="post" action="/account/email"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Email<input name="email" type="email" value="{{.User.Email}}" required autocomplete="email"></label><button type="submit">Save email</button></form></section><section class="panel settings-section" id="security"><h2>Security</h2><form method="post" action="/account/password"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Current password<input name="current_password" type="password" required autocomplete="current-password"></label><label>New password<input name="new_password" type="password" required minlength="12" autocomplete="new-password"></label><label>Confirm new password<input name="confirm_password" type="password" required minlength="12" autocomplete="new-password"></label><button type="submit">Change password</button></form></section><section class="panel settings-section" id="language"><h2>Language</h2><form method="post" action="/account/language"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Language<select name="language">{{range .Languages}}<option value="{{.Code}}"{{if eq $.Language .Code}} selected{{end}}>{{.Display}}</option>{{end}}</select></label><button type="submit">Save language</button></form></section><section class="panel settings-section" id="notifications"><h2>Email notifications</h2><p>Important failures are enabled by default. Routine start, stop and backup messages are disabled until you opt in.</p><form method="post" action="/account/notifications"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label class="check"><input type="checkbox" name="server_start"{{if index .NotificationPreferences "server_start"}} checked{{end}}> Server started</label><label class="check"><input type="checkbox" name="server_stop"{{if index .NotificationPreferences "server_stop"}} checked{{end}}> Server stopped</label><label class="check"><input type="checkbox" name="server_error"{{if index .NotificationPreferences "server_error"}} checked{{end}}> Server errors</label><label class="check"><input type="checkbox" name="backup"{{if index .NotificationPreferences "backup"}} checked{{end}}> Backups</label><label class="check"><input type="checkbox" name="restore"{{if index .NotificationPreferences "restore"}} checked{{end}}> Restores</label><label class="check"><input type="checkbox" name="update"{{if index .NotificationPreferences "update"}} checked{{end}}> Updates</label><label class="check"><input type="checkbox" name="administration"{{if index .NotificationPreferences "administration"}} checked{{end}}> Important administrative actions</label><button type="submit">Save notification preferences</button></form></section></main></body></html>{{end}}
{{define "account.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Settings · DoGaMa</title><link rel="stylesheet" href="/static/app.v1.css"></head><body class="app-body">{{template "sidebar" .}}<main class="app-main"><div class="page-heading"><h1>Settings</h1><p>Personal account preferences.</p></div><nav class="tabs"><a href="#profile">Profile</a><a href="#security">Security</a><a href="#language">Language</a><a href="#notifications">Notifications</a></nav><section class="panel settings-section" id="profile"><h2>Profile</h2><p>Signed in as <strong>{{.User.Username}}</strong>.</p><form method="post" action="/account/email"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Email<input name="email" type="email" value="{{.User.Email}}" required autocomplete="email"></label><button type="submit">Save email</button></form></section><section class="panel settings-section" id="security"><h2>Security</h2><form method="post" action="/account/password"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Current password<input name="current_password" type="password" required autocomplete="current-password"></label><label>New password<input name="new_password" type="password" required minlength="12" autocomplete="new-password"></label><label>Confirm new password<input name="confirm_password" type="password" required minlength="12" autocomplete="new-password"></label><button type="submit">Change password</button></form></section><section class="panel settings-section" id="language"><h2>Language</h2><form method="post" action="/account/language"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>Language<select name="language">{{range .Languages}}<option value="{{.Code}}"{{if eq $.Language .Code}} selected{{end}}>{{.Display}}</option>{{end}}</select></label><button type="submit">Save language</button></form></section><section class="panel settings-section" id="notifications"><h2>Email notifications</h2><p>Important failures are enabled by default. Routine start, stop and backup messages are disabled until you opt in.</p><form method="post" action="/account/notifications"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label class="check"><input type="checkbox" name="server_start"{{if index .NotificationPreferences "server_start"}} checked{{end}}> Server started</label><label class="check"><input type="checkbox" name="server_stop"{{if index .NotificationPreferences "server_stop"}} checked{{end}}> Server stopped</label><label class="check"><input type="checkbox" name="server_error"{{if index .NotificationPreferences "server_error"}} checked{{end}}> Server errors</label><label class="check"><input type="checkbox" name="backup"{{if index .NotificationPreferences "backup"}} checked{{end}}> Backups</label><label class="check"><input type="checkbox" name="restore"{{if index .NotificationPreferences "restore"}} checked{{end}}> Restores</label><label class="check"><input type="checkbox" name="update"{{if index .NotificationPreferences "update"}} checked{{end}}> Updates</label><label class="check"><input type="checkbox" name="administration"{{if index .NotificationPreferences "administration"}} checked{{end}}> Important administrative actions</label><button type="submit">Save notification preferences</button></form></section></main><script src="/static/app.v2.js"></script></body></html>{{end}}
+1 -1
View File
@@ -1 +1 @@
{{define "audit.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Title}} · DoGaMa</title><link rel="stylesheet" href="/static/app.v2.css"><link rel="stylesheet" href="/static/theme.v2.css"></head><body class="app-body">{{template "sidebar" .}}<main class="app-main"><header class="page-heading audit-header"><div><p class="eyebrow">{{.Msg "audit.eyebrow"}}</p><h1>{{.Msg "audit.title"}}</h1><p>{{.AuditEventCount}} {{.Msg "audit.events"}} · {{.Msg "audit.timezone"}} {{.AuditFilter.Timezone}}</p></div></header><section class="panel audit-panel"><div class="section-heading"><div><p class="eyebrow">{{.Msg "audit.filters"}}</p><h2>{{.Msg "audit.filter"}}</h2></div><a class="button-secondary link-button" href="/audit">{{.Msg "audit.reset"}}</a></div><form class="audit-filter-grid" method="get" action="/audit"><label>{{.Msg "audit.period"}}<select name="period"><option value="">{{.Msg "audit.any"}}</option><option value="today" {{if eq .AuditFilter.Period "today"}}selected{{end}}>{{.Msg "audit.today"}}</option><option value="7d" {{if eq .AuditFilter.Period "7d"}}selected{{end}}>{{.Msg "audit.last_7_days"}}</option><option value="30d" {{if eq .AuditFilter.Period "30d"}}selected{{end}}>{{.Msg "audit.last_30_days"}}</option><option value="custom" {{if eq .AuditFilter.Period "custom"}}selected{{end}}>{{.Msg "audit.custom"}}</option></select></label><label>{{.Msg "audit.from"}}<input name="from" type="date" value="{{.AuditFilter.From}}"></label><label>{{.Msg "audit.to"}}<input name="to" type="date" value="{{.AuditFilter.To}}"></label><label>{{.Msg "audit.actor"}}<select name="actor_id"><option value="">{{.Msg "audit.any"}}</option>{{range .AuditActors}}<option value="{{.ID}}" {{if eq $.AuditFilter.ActorID .ID}}selected{{end}}>{{.Label}}</option>{{end}}</select></label><label>{{.Msg "audit.instance"}}<select name="instance_id"><option value="">{{.Msg "audit.any"}}</option>{{range .Instances}}<option value="{{.ID}}" {{if eq $.AuditFilter.InstanceID .ID}}selected{{end}}>{{.Preview.DisplayName}}</option>{{end}}</select></label><label>{{.Msg "audit.action"}}<select name="action"><option value="">{{.Msg "audit.any"}}</option>{{range .AuditActions}}<option value="{{.}}" {{if eq $.AuditFilter.Action .}}selected{{end}}>{{.}}</option>{{end}}</select></label><label>{{.Msg "audit.outcome"}}<select name="outcome"><option value="">{{.Msg "audit.any"}}</option><option value="allowed" {{if eq .AuditFilter.Outcome "allowed"}}selected{{end}}>{{.Msg "audit.allowed"}}</option><option value="denied" {{if eq .AuditFilter.Outcome "denied"}}selected{{end}}>{{.Msg "audit.failed"}}</option><option value="failed" {{if eq .AuditFilter.Outcome "failed"}}selected{{end}}>{{.Msg "audit.failed"}}</option></select></label><label>{{.Msg "audit.search"}}<input name="q" type="search" value="{{.AuditFilter.Query}}" maxlength="100"></label><button type="submit">{{.Msg "audit.filter"}}</button></form></section><section class="panel audit-results"><div class="section-heading"><div><p class="eyebrow">{{.Msg "audit.results"}}</p><h2>{{.AuditEventCount}} {{.Msg "audit.events"}}</h2></div><p class="muted">{{.Msg "audit.timezone"}} {{.AuditFilter.Timezone}}</p></div>{{if .AuditEvents}}<div class="audit-list">{{range .AuditEvents}}<article class="audit-event"><time datetime="{{.UTC}}" title="{{.UTC}}">{{.Time}}</time><div><strong>{{.ActionLabel}}</strong><small><code>{{.Action}}</code> · {{.OutcomeLabel}}</small></div><div><strong>{{if .Actor}}{{.Actor}}{{else}}—{{end}}</strong><small>{{if .Instance}}{{.Instance}}{{else}}—{{end}}</small></div>{{if .Details}}<details><summary>{{$.Msg "audit.details"}}</summary><p>{{.Details}}</p></details>{{end}}</article>{{end}}</div>{{else}}<div class="empty-state"><span class="empty-icon">◈</span><h2>{{if .AuditQuery}}{{.Msg "audit.empty_filtered"}}{{else}}{{.Msg "audit.empty"}}{{end}}</h2></div>{{end}}{{if gt .AuditPages 1}}<nav class="audit-pagination" aria-label="{{.Msg "audit.pagination"}}">{{if gt .AuditPage 1}}<a class="button-secondary link-button" href="/audit?{{.AuditQuery}}&amp;page={{minus .AuditPage 1}}">{{.Msg "audit.previous"}}</a>{{end}}<span>{{.AuditPage}} / {{.AuditPages}}</span>{{if lt .AuditPage .AuditPages}}<a class="button-secondary link-button" href="/audit?{{.AuditQuery}}&amp;page={{plus .AuditPage 1}}">{{.Msg "audit.next"}}</a>{{end}}</nav>{{end}}</section></main></body></html>{{end}}
{{define "audit.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Title}} · DoGaMa</title><link rel="stylesheet" href="/static/app.v2.css"><link rel="stylesheet" href="/static/theme.v2.css"></head><body class="app-body">{{template "sidebar" .}}<main class="app-main"><header class="page-heading audit-header"><div><p class="eyebrow">{{.Msg "audit.eyebrow"}}</p><h1>{{.Msg "audit.title"}}</h1><p>{{.AuditEventCount}} {{.Msg "audit.events"}} · {{.Msg "audit.timezone"}} {{.AuditFilter.Timezone}}</p></div></header><section class="panel audit-panel"><div class="section-heading"><div><p class="eyebrow">{{.Msg "audit.filters"}}</p><h2>{{.Msg "audit.filter"}}</h2></div><a class="button-secondary link-button" href="/audit">{{.Msg "audit.reset"}}</a></div><form class="audit-filter-grid" method="get" action="/audit"><label>{{.Msg "audit.period"}}<select name="period"><option value="">{{.Msg "audit.any"}}</option><option value="today" {{if eq .AuditFilter.Period "today"}}selected{{end}}>{{.Msg "audit.today"}}</option><option value="7d" {{if eq .AuditFilter.Period "7d"}}selected{{end}}>{{.Msg "audit.last_7_days"}}</option><option value="30d" {{if eq .AuditFilter.Period "30d"}}selected{{end}}>{{.Msg "audit.last_30_days"}}</option><option value="custom" {{if eq .AuditFilter.Period "custom"}}selected{{end}}>{{.Msg "audit.custom"}}</option></select></label><label>{{.Msg "audit.from"}}<input name="from" type="date" value="{{.AuditFilter.From}}"></label><label>{{.Msg "audit.to"}}<input name="to" type="date" value="{{.AuditFilter.To}}"></label><label>{{.Msg "audit.actor"}}<select name="actor_id"><option value="">{{.Msg "audit.any"}}</option>{{range .AuditActors}}<option value="{{.ID}}" {{if eq $.AuditFilter.ActorID .ID}}selected{{end}}>{{.Label}}</option>{{end}}</select></label><label>{{.Msg "audit.instance"}}<select name="instance_id"><option value="">{{.Msg "audit.any"}}</option>{{range .Instances}}<option value="{{.ID}}" {{if eq $.AuditFilter.InstanceID .ID}}selected{{end}}>{{.Preview.DisplayName}}</option>{{end}}</select></label><label>{{.Msg "audit.action"}}<select name="action"><option value="">{{.Msg "audit.any"}}</option>{{range .AuditActions}}<option value="{{.}}" {{if eq $.AuditFilter.Action .}}selected{{end}}>{{.}}</option>{{end}}</select></label><label>{{.Msg "audit.outcome"}}<select name="outcome"><option value="">{{.Msg "audit.any"}}</option><option value="allowed" {{if eq .AuditFilter.Outcome "allowed"}}selected{{end}}>{{.Msg "audit.allowed"}}</option><option value="denied" {{if eq .AuditFilter.Outcome "denied"}}selected{{end}}>{{.Msg "audit.failed"}}</option><option value="failed" {{if eq .AuditFilter.Outcome "failed"}}selected{{end}}>{{.Msg "audit.failed"}}</option></select></label><label>{{.Msg "audit.search"}}<input name="q" type="search" value="{{.AuditFilter.Query}}" maxlength="100"></label><button type="submit">{{.Msg "audit.filter"}}</button></form></section><section class="panel audit-results"><div class="section-heading"><div><p class="eyebrow">{{.Msg "audit.results"}}</p><h2>{{.AuditEventCount}} {{.Msg "audit.events"}}</h2></div><p class="muted">{{.Msg "audit.timezone"}} {{.AuditFilter.Timezone}}</p></div>{{if .AuditEvents}}<div class="audit-list">{{range .AuditEvents}}<article class="audit-event"><time datetime="{{.UTC}}" title="{{.UTC}}">{{.Time}}</time><div><strong>{{.ActionLabel}}</strong><small><code>{{.Action}}</code> · {{.OutcomeLabel}}</small></div><div><strong>{{if .Actor}}{{.Actor}}{{else}}—{{end}}</strong><small>{{if .Instance}}{{.Instance}}{{else}}—{{end}}</small></div>{{if .Details}}<details><summary>{{$.Msg "audit.details"}}</summary><p>{{.Details}}</p></details>{{end}}</article>{{end}}</div>{{else}}<div class="empty-state"><span class="empty-icon">◈</span><h2>{{if .AuditQuery}}{{.Msg "audit.empty_filtered"}}{{else}}{{.Msg "audit.empty"}}{{end}}</h2></div>{{end}}{{if gt .AuditPages 1}}<nav class="audit-pagination" aria-label="{{.Msg "audit.pagination"}}">{{if gt .AuditPage 1}}<a class="button-secondary link-button" href="/audit?{{.AuditQuery}}&amp;page={{minus .AuditPage 1}}">{{.Msg "audit.previous"}}</a>{{end}}<span>{{.AuditPage}} / {{.AuditPages}}</span>{{if lt .AuditPage .AuditPages}}<a class="button-secondary link-button" href="/audit?{{.AuditQuery}}&amp;page={{plus .AuditPage 1}}">{{.Msg "audit.next"}}</a>{{end}}</nav>{{end}}</section></main><script src="/static/app.v2.js"></script></body></html>{{end}}
+22 -1
View File
@@ -1 +1,22 @@
{{define "sidebar"}}<aside class="sidebar" id="app-navigation"><a class="brand brand-image" href="/"><img src="/static/dogama.png" alt="DoGaMa"></a><nav aria-label="{{.Msg "nav.primary"}}"><a class="nav-dashboard {{if eq .ActivePage "dashboard"}}active{{end}}" href="/">{{.Msg "nav.dashboard"}}</a><a class="nav-catalog {{if eq .ActivePage "catalog"}}active{{end}}" href="/catalog">{{.Msg "nav.catalog"}}</a>{{if .IsAdmin}}<a class="nav-audit {{if eq .ActivePage "audit"}}active{{end}}" href="/audit">{{.Msg "nav.audit"}}</a><a class="nav-administration {{if eq .ActivePage "administration"}}active{{end}}" href="/administration">Administration</a>{{end}}<a class="nav-account {{if eq .ActivePage "account"}}active{{end}}" href="/account">⚙ {{.Msg "nav.settings"}}</a></nav><div class="account"><p class="sr-only">{{.Msg "account.signed_in"}} <strong>{{.User.Username}}</strong>.</p><div class="account-card"><span class="avatar">DG</span><span><strong>{{.User.Username}}</strong><small>{{.User.Role}}</small></span></div><form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button class="signout-button" type="submit">↪ {{.Msg "logout.submit"}}</button></form></div></aside>{{end}}
{{define "sidebar"}}
<link rel="stylesheet" href="/static/mobile-fixes.css">
<header class="mobile-header">
<a href="/"><img src="/static/dogama.png" alt="DoGaMa"></a>
<button class="menu-button" type="button" aria-controls="app-navigation" aria-expanded="false">Menu</button>
</header>
<aside class="sidebar" id="app-navigation">
<a class="brand brand-image" href="/"><img src="/static/dogama.png" alt="DoGaMa"></a>
<nav aria-label="{{.Msg "nav.primary"}}">
<a class="nav-dashboard {{if eq .ActivePage "dashboard"}}active{{end}}" href="/">{{.Msg "nav.dashboard"}}</a>
<a class="nav-catalog {{if eq .ActivePage "catalog"}}active{{end}}" href="/catalog">{{.Msg "nav.catalog"}}</a>
{{if .IsAdmin}}<a class="nav-audit {{if eq .ActivePage "audit"}}active{{end}}" href="/audit">{{.Msg "nav.audit"}}</a>
<a class="nav-administration {{if eq .ActivePage "administration"}}active{{end}}" href="/administration">Administration</a>{{end}}
<a class="nav-account {{if eq .ActivePage "account"}}active{{end}}" href="/account">⚙ {{.Msg "nav.settings"}}</a>
</nav>
<div class="account">
<p class="sr-only">{{.Msg "account.signed_in"}} <strong>{{.User.Username}}</strong>.</p>
<div class="account-card"><span class="avatar">DG</span><span><strong>{{.User.Username}}</strong><small>{{.User.Role}}</small></span></div>
<form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button class="signout-button" type="submit">↪ {{.Msg "logout.submit"}}</button></form>
</div>
</aside>
{{end}}
+1 -1
View File
@@ -16,5 +16,5 @@
</section>
<section class="panel settings-section" id="audit"><h2>{{.Msg "settings.audit_retention"}}</h2><p>{{.Msg "settings.audit_help"}}</p><a class="button-secondary link-button" href="/audit">{{.Msg "settings.view_audit"}}</a><form method="post" action="/admin/audit-policy"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "settings.retention_days"}}<input name="retention_days" type="number" min="0" value="{{.AuditPolicy.RetentionDays}}"></label><label>{{.Msg "settings.maximum_entries"}}<input name="maximum_count" type="number" min="0" value="{{.AuditPolicy.MaximumCount}}"></label><p class="help">{{.Msg "settings.zero_unlimited"}}</p><button type="submit">{{.Msg "settings.save_retention"}}</button></form><form method="post" action="/admin/audit-purge"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "settings.delete_before"}}<input required name="before" type="date"></label><label class="check"><input required name="confirm" value="yes" type="checkbox"> {{.Msg "settings.confirm_purge"}}</label><button type="submit">{{.Msg "settings.purge"}}</button></form></section>
<section class="panel settings-section" id="containers"><h2>{{.Msg "settings.container_labels"}}</h2><p>{{.Msg "settings.container_labels_help"}}</p><form method="post" action="/admin/game-container-labels"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "settings.global_labels"}}<textarea name="labels" rows="8">{{.GlobalLabels}}</textarea></label><fieldset><legend>{{.Msg "settings.apply"}}</legend><label><input type="radio" name="apply" value="next_start" checked> {{.Msg "settings.next_start"}}</label><label><input type="radio" name="apply" value="immediate"> {{.Msg "settings.immediate"}}</label></fieldset><button type="submit">{{.Msg "settings.save_labels"}}</button></form></section>
</main></body></html>
</main><script src="/static/app.v2.js"></script></body></html>
{{end}}
@@ -5,5 +5,5 @@
<nav class="tabs" aria-label="{{.Msg "settings.tabs"}}"><a href="/administration/users">{{.Msg "template_repositories.users"}}</a><a href="/administration#notifications">{{.Msg "settings.notifications"}}</a><a href="/administration/template-repositories">{{.Msg "template_repositories.heading"}}</a><a href="/administration#audit">{{.Msg "settings.audit"}}</a></nav>
<section class="panel settings-section"><details{{if .Error}} open{{end}}><summary class="button-secondary link-button">{{.Msg "template_repositories.new"}}</summary>{{if .Error}}<p role="alert">{{.Error}}</p>{{end}}<form method="post" action="/administration/template-repositories"><div class="form-grid"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "template_repositories.name"}}<input required maxlength="120" name="name" value="{{.TemplateRepositoryInput.Name}}"></label><label>{{.Msg "template_repositories.url"}}<input required maxlength="2048" type="url" name="url" value="{{.TemplateRepositoryInput.URL}}" placeholder="https://templates.example.com"></label></div><button type="submit">{{.Msg "template_repositories.add"}}</button></form></details></section>
<section class="panel settings-section"><h2>{{.Msg "template_repositories.heading"}}</h2><p>{{.Msg "template_repositories.not_implemented"}}</p>{{if .TemplateRepositories}}<ul class="channel-list">{{range .TemplateRepositories}}<li><strong>{{.Name}}</strong><small>{{.URL}} · {{$.Msg "template_repositories.not_implemented"}}</small><form method="post" action="/administration/template-repositories/{{.ID}}/delete" onsubmit="return confirm('{{$.Msg "template_repositories.delete_confirm"}}');"><input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"><button type="submit" aria-label="{{$.Msg "template_repositories.delete"}} {{.Name}}">🗑 {{$.Msg "template_repositories.delete"}}</button></form></li>{{end}}</ul>{{else}}<p>{{.Msg "template_repositories.empty"}}</p>{{end}}</section>
</main></body></html>
</main><script src="/static/app.v2.js"></script></body></html>
{{end}}
+1 -1
View File
@@ -21,5 +21,5 @@
{{else}}<p>Administrators have implicit access to every instance.</p>{{end}}
</section>
{{else}}<section class="panel"><p>No users found.</p></section>{{end}}
</main></body></html>
</main><script src="/static/app.v2.js"></script></body></html>
{{end}}