chore(code): improve source readability
CI / validate (pull_request) Successful in 26m0s

This commit is contained in:
2026-08-15 23:09:59 +02:00
parent 99a79682dc
commit 39a33556d6
19 changed files with 3687 additions and 297 deletions
+4
View File
@@ -2,3 +2,7 @@
# equivalent local directories if a developer exports them for inspection.
deploy/dev/caddy-data/
deploy/dev/caddy-config/
# Local runtime and browser-validation artifacts.
data/
.playwright-cli/
+1
View File
@@ -5,6 +5,7 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Baseline
- Current reference: post-v0.1.1 release-gate correction based on tagged baseline `88a45c7`.
- Source-facing HTML templates, CSS and browser JavaScript are structurally formatted for maintainability; this formatting-only baseline preserves their existing contracts and behavior.
- SQLite is initialized directly from one embedded current schema and can be reopened safely after first initialization. Development databases from earlier revisions are intentionally unsupported until versioned production migrations are introduced.
- Roadmap milestones 1-10 are implemented; this is the V1 feature baseline.
File diff suppressed because one or more lines are too long
+23 -3
View File
@@ -34,10 +34,30 @@ if (deploymentForm) {
deploymentForm.addEventListener("submit", (event) => {
event.preventDefault();
const submit = deploymentForm.querySelector("[data-deployment-submit]");
if (submit) { submit.disabled = true; submit.textContent = "…"; }
if (submit) {
submit.disabled = true;
submit.textContent = "…";
}
let progress = document.querySelector("[data-deployment-progress]");
if (!progress) { progress = document.createElement("section"); progress.className = "panel deployment-progress"; progress.dataset.deploymentProgress = ""; deploymentForm.after(progress); }
progress.replaceChildren(); const heading = document.createElement("h2"); heading.textContent = "Deployment progress"; const status = document.createElement("p"); status.dataset.operationStatus = ""; const list = document.createElement("ol"); list.dataset.operationSteps = ""; const error = document.createElement("p"); error.className = "error"; error.hidden = true; const link = document.createElement("a"); link.hidden = true; progress.append(heading, status, list, error, link);
if (!progress) {
progress = document.createElement("section");
progress.className = "panel deployment-progress";
progress.dataset.deploymentProgress = "";
deploymentForm.after(progress);
}
progress.replaceChildren();
const heading = document.createElement("h2");
heading.textContent = "Deployment progress";
const status = document.createElement("p");
status.dataset.operationStatus = "";
const list = document.createElement("ol");
list.dataset.operationSteps = "";
const error = document.createElement("p");
error.className = "error";
error.hidden = true;
const link = document.createElement("a");
link.hidden = true;
progress.append(heading, status, list, error, link);
fetch(deploymentForm.action || window.location.href, { method: "POST", body: new FormData(deploymentForm), credentials: "same-origin", headers: { Accept: "application/json" } }).then((response) => response.ok ? response.json() : Promise.reject(new Error("Deployment could not be queued."))).then(({ operation_id, instance_id }) => {
progress.hidden = false; deploymentForm.hidden = true;
const poll = () => fetch(`/api/v1/operations/${encodeURIComponent(operation_id)}`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : Promise.reject(new Error("Progress unavailable."))).then((operation) => {
+12 -3
View File
@@ -1,5 +1,14 @@
@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; }
html, body {
overflow-x: hidden;
}
.mobile-header {
width: 100vw;
margin-left: calc(50% - 50vw);
margin-right: 0;
}
.tabs {
flex-wrap: wrap;
overflow-x: visible;
}
}
+382 -203
View File
@@ -1,279 +1,458 @@
.app-main {
isolation: isolate;
position: relative;
isolation: isolate;
position: relative;
}
.app-main::before {
background: url("/static/dogama-logo.png") center / min(58vw, 720px) no-repeat;
content: "";
filter: saturate(.8);
inset: 0;
opacity: .025;
pointer-events: none;
position: fixed;
z-index: 0;
background: url("/static/dogama-logo.png") center / min(58vw, 720px) no-repeat;
content: "";
filter: saturate(.8);
inset: 0;
opacity: .025;
pointer-events: none;
position: fixed;
z-index: 0;
}
.app-main::after {
background-image:
linear-gradient(90deg, #20d9f312 1px, transparent 1px),
linear-gradient(#f02fb80d 1px, transparent 1px);
background-size: 64px 64px;
bottom: 0;
content: "";
height: 34vh;
left: 220px;
mask-image: linear-gradient(transparent, #000);
opacity: .45;
perspective: 400px;
pointer-events: none;
position: fixed;
right: 0;
transform: perspective(380px) rotateX(62deg) scale(1.25);
transform-origin: bottom;
z-index: 0;
linear-gradient(90deg, #20d9f312 1px, transparent 1px),
linear-gradient(#f02fb80d 1px, transparent 1px);
background-size: 64px 64px;
bottom: 0;
content: "";
height: 34vh;
left: 220px;
mask-image: linear-gradient(transparent, #000);
opacity: .45;
perspective: 400px;
pointer-events: none;
position: fixed;
right: 0;
transform: perspective(380px) rotateX(62deg) scale(1.25);
transform-origin: bottom;
z-index: 0;
}
.app-main > * {
position: relative;
z-index: 1;
position: relative;
z-index: 1;
}
.sidebar {
padding: 26px 8px 18px;
width: 200px;
padding: 26px 8px 18px;
width: 200px;
}
.app-main {
margin-left: 200px;
margin-left: 200px;
}
.app-main::after {
left: 200px;
left: 200px;
}
.brand {
gap: 8px;
padding: 0 8px 28px;
gap: 8px;
padding: 0 8px 28px;
}
.brand img {
height: 76px;
width: 76px;
height: 76px;
width: 76px;
}
.brand strong {
font-size: 1.45rem;
font-size: 1.45rem;
}
.brand small {
display: none !important;
display: none !important;
}
.sidebar nav {
gap: 2px;
margin: 8px -8px 0;
gap: 2px;
margin: 8px -8px 0;
}
.sidebar nav a,
.sidebar nav span.disabled {
align-items: center;
display: flex;
gap: 11px;
min-height: 52px;
padding-left: 24px;
padding-right: 20px;
align-items: center;
display: flex;
gap: 11px;
min-height: 52px;
padding-left: 24px;
padding-right: 20px;
}
.sidebar nav a.active {
border-left-color: var(--accent-cyan);
border-radius: 0 10px 10px 0;
border-right-color: var(--accent-magenta);
box-shadow: inset 4px 0 var(--accent-cyan), inset -3px 0 var(--accent-magenta), 0 0 22px #cf2fc326;
border-left-color: var(--accent-cyan);
border-radius: 0 10px 10px 0;
border-right-color: var(--accent-magenta);
box-shadow: inset 4px 0 var(--accent-cyan), inset -3px 0 var(--accent-magenta), 0 0 22px #cf2fc326;
}
.sidebar nav a::before,
.sidebar nav a: :before,
.sidebar nav span.disabled::before {
color: #a9b4ca;
display: grid;
flex: 0 0 22px;
font-size: 1rem;
height: 22px;
place-items: center;
width: 22px;
color: #a9b4ca;
display: grid;
flex: 0 0 22px;
font-size: 1rem;
height: 22px;
place-items: center;
width: 22px;
}
.sidebar nav a.active::before {
color: var(--accent-cyan);
color: var(--accent-cyan);
}
.nav-instances::before {
content: "\25A4";
}
.nav-catalog::before {
content: "\2667";
}
.nav-backups::before {
content: "\25A3";
}
.nav-settings::after {
content: "\203A";
margin-left: auto;
}
.nav-instances::before { content: "\25A4"; }
.nav-catalog::before { content: "\2667"; }
.nav-backups::before { content: "\25A3"; }
.nav-settings::after { content: "\203A"; margin-left: auto; }
.sidebar nav span.disabled {
border: 1px solid transparent;
border-radius: 10px;
color: #a5aec2;
cursor: not-allowed;
font-weight: 650;
opacity: .72;
padding-bottom: 11px;
padding-top: 11px;
border: 1px solid transparent;
border-radius: 10px;
color: #a5aec2;
cursor: not-allowed;
font-weight: 650;
opacity: .72;
padding-bottom: 11px;
padding-top: 11px;
}
.account-card {
background: #0d1120cc;
border: 1px solid #252c45;
border-radius: 10px;
margin-bottom: 10px;
padding: 13px 12px;
background: #0d1120cc;
border: 1px solid #252c45;
border-radius: 10px;
margin-bottom: 10px;
padding: 13px 12px;
}
.account .avatar {
background: #151126;
border: 1px solid var(--accent-magenta);
color: #ef54cf;
height: 42px;
width: 42px;
background: #151126;
border: 1px solid var(--accent-magenta);
color: #ef54cf;
height: 42px;
width: 42px;
}
.account-chevron {
color: var(--text-secondary);
margin-left: auto;
color: var(--text-secondary);
margin-left: auto;
}
.account form {
border-top: 1px solid #22283e;
padding-top: 10px;
border-top: 1px solid #22283e;
padding-top: 10px;
}
.account .signout-button {
align-items: center;
background: transparent;
border: 0;
box-shadow: none;
color: var(--text-secondary);
display: flex;
gap: 11px;
padding: 10px 12px;
width: 100%;
align-items: center;
background: transparent;
border: 0;
box-shadow: none;
color: var(--text-secondary);
display: flex;
gap: 11px;
padding: 10px 12px;
width: 100%;
}
.account .signout-button:hover {
color: var(--text-primary);
color: var(--text-primary);
}
.nav-dashboard::before {
content: "⌂";
}
.nav-audit::before {
content: "▣";
}
.nav-settings::before {
content: "⚙";
}
.nav-dashboard::before { content: "⌂"; }
.nav-audit::before { content: "▣"; }
.nav-settings::before { content: "⚙"; }
.page-heading {
align-items: center;
gap: 24px;
align-items: center;
gap: 24px;
}
.instance-search {
max-width: 320px;
position: relative;
width: min(100%, 320px);
max-width: 320px;
position: relative;
width: min(100%, 320px);
}
.instance-search input {
background: #090c19cc;
border-color: #3c4565;
border-radius: 9px;
padding-left: 42px;
padding-right: 42px;
background: #090c19cc;
border-color: #3c4565;
border-radius: 9px;
padding-left: 42px;
padding-right: 42px;
}
.instance-search::before {
color: var(--text-secondary);
content: "⌕";
font-size: 1.35rem;
left: 13px;
position: absolute;
top: 5px;
z-index: 1;
color: var(--text-secondary);
content: "⌕";
font-size: 1.35rem;
left: 13px;
position: absolute;
top: 5px;
z-index: 1;
}
.instance-search kbd {
background: #15192b;
border: 1px solid #313753;
border-radius: 5px;
color: var(--text-secondary);
font: inherit;
padding: 0 7px;
position: absolute;
right: 10px;
top: 9px;
background: #15192b;
border: 1px solid #313753;
border-radius: 5px;
color: var(--text-secondary);
font: inherit;
padding: 0 7px;
position: absolute;
right: 10px;
top: 9px;
}
.summary-grid {
margin: 0 auto 22px;
margin: 0 auto 22px;
}
.summary-grid article {
background: linear-gradient(145deg, #111628dd, #0b0f1ddd);
min-height: 76px;
background: linear-gradient(145deg, #111628dd, #0b0f1ddd);
min-height: 76px;
}
.instance-grid {
position: relative;
position: relative;
}
.instance-card[hidden] {
display: none;
display: none;
}
.search-empty {
color: var(--text-secondary);
padding: 48px 20px;
text-align: center;
color: var(--text-secondary);
padding: 48px 20px;
text-align: center;
}
button,
.link-button {
background: linear-gradient(100deg, #782fd9, #e52eb0 65%, #14c9ed);
border: 1px solid #ef45c2;
box-shadow: 0 0 18px #f02fb81f;
background: linear-gradient(100deg, #782fd9, #e52eb0 65%, #14c9ed);
border: 1px solid #ef45c2;
box-shadow: 0 0 18px #f02fb81f;
}
.button-secondary {
background: #11162a;
border-color: #38415f;
background: #11162a;
border-color: #38415f;
box-shadow: none;
}
.tabs {
background: #0c1020dd;
border-color: #303955;
}
@media (max-width: 760px) {
.app-main::after {
left: 0;
}
.page-heading {
align-items: stretch;
display: grid;
}
.instance-search {
max-width: none;
width: 100%;
}
}
.brand.brand-image {
display: block;
padding: 0 14px 28px;
}
.brand.brand-image img {
display: block;
width: min(100%, 156px);
height: auto;
border-radius: 0;
box-shadow: none;
}
.tabs {
background: #0c1020dd;
border-color: #303955;
.dashboard-header {
margin-bottom: 26px;
}
@media (max-width: 760px) {
.app-main::after { left: 0; }
.page-heading { align-items: stretch; display: grid; }
.instance-search { max-width: none; width: 100%; }
.dashboard-section {
max-width: 1400px;
margin: 0 auto 24px;
}
.summary-grid {
grid-template-columns: repeat(5, minmax(0,1fr));
}
.summary-card {
min-width: 0;
overflow: hidden;
position: relative;
}
.summary-card::after {
content: "";
position: absolute;
inset: auto -12px -32px auto;
width: 70px;
height: 70px;
border-radius: 50%;
background: currentColor;
filter: blur(26px);
opacity: .18;
}
.summary-card .summary-icon {
float: right;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 1px solid currentColor;
border-radius: 9px;
font-size: 1rem;
}
.summary-card.total {
color: var(--accent-cyan);
}
.summary-card.running {
color: var(--status-running);
}
.summary-card.stopped {
color: var(--status-error);
}
.summary-card.updating {
color: var(--status-warning);
}
.summary-card.error {
color: #ff3c5f;
}
.summary-card > span:not(.summary-icon) {
color: var(--text-secondary);
}
.summary-card strong {
display: block;
color: var(--text-primary);
}
.instance-card h3 {
margin: 5px 0 7px;
font-size: 1.16rem;
}
.dashboard-lower {
display: grid;
grid-template-columns: repeat(2,minmax(0,1fr));
gap: 22px;
max-width: 1400px;
margin: auto;
}
.dashboard-lower .panel {
margin: 0;
max-width: none;
}
.panel-empty {
color: var(--text-secondary);
margin: 0;
padding: 26px 0;
}
.activity-list {
display: grid;
gap: 14px;
list-style: none;
margin: 0;
padding: 0;
}
.activity-list li {
display: grid;
grid-template-columns: 32px minmax(0,1fr) auto;
align-items: center;
gap: 11px;
padding-bottom: 14px;
border-bottom: 1px solid #262d47;
}
.activity-list li:last-child {
border: 0;
padding-bottom: 0;
}
.activity-icon {
display: grid;
place-items: center;
width: 30px;
height: 30px;
color: var(--accent-cyan);
background: #102237;
border: 1px solid #245574;
border-radius: 8px;
}
.activity-list strong,.activity-list span {
display: block;
}
.activity-list div span,.activity-list time {
color: var(--text-secondary);
font-size: .78rem;
}
.activity-list time {
white-space: nowrap;
}
.system-list {
display: grid;
gap: 0;
margin: 0;
}
.system-list > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 44px;
border-bottom: 1px solid #262d47;
}
.system-list > div:last-child {
border: 0;
}
.system-list dt {
color: var(--text-secondary);
}
.system-list dd {
display: flex;
align-items: center;
justify-content: end;
gap: 8px;
margin: 0;
text-align: right;
}
.system-list .is-good::before,.system-list .is-bad::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
}
.system-list .is-good {
color: var(--status-running);
}
.system-list .is-bad {
color: var(--status-error);
}
.storage-track {
display: block;
width: 90px;
height: 6px;
overflow: hidden;
border-radius: 99px;
background: #272e49;
}
.storage-track i {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg,var(--accent-cyan),var(--accent-magenta));
}
@media (max-width: 1050px) {
.summary-grid {
grid-template-columns: repeat(3,minmax(0,1fr));
}
.dashboard-lower {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
.summary-grid {
grid-template-columns: repeat(2,minmax(0,1fr));
}
.summary-card:last-child {
grid-column: span 2;
}
.activity-list li {
grid-template-columns: 32px minmax(0,1fr);
}
.activity-list time {
grid-column: 2;
}
.system-list > div {
align-items: flex-start;
flex-direction: column;
padding: 10px 0;
gap: 3px;
}
.system-list dd {
justify-content: start;
text-align: left;
}
}
.brand.brand-image { display:block; padding:0 14px 28px; }
.brand.brand-image img { display:block; width:min(100%, 156px); height:auto; border-radius:0; box-shadow:none; }
.dashboard-header { margin-bottom:26px; }
.dashboard-section { max-width:1400px; margin:0 auto 24px; }
.summary-grid { grid-template-columns:repeat(5, minmax(0,1fr)); }
.summary-card { min-width:0; overflow:hidden; position:relative; }
.summary-card::after { content:""; position:absolute; inset:auto -12px -32px auto; width:70px; height:70px; border-radius:50%; background:currentColor; filter:blur(26px); opacity:.18; }
.summary-card .summary-icon { float:right; display:grid; place-items:center; width:32px; height:32px; border:1px solid currentColor; border-radius:9px; font-size:1rem; }
.summary-card.total { color:var(--accent-cyan); }.summary-card.running { color:var(--status-running); }.summary-card.stopped { color:var(--status-error); }.summary-card.updating { color:var(--status-warning); }.summary-card.error { color:#ff3c5f; }
.summary-card > span:not(.summary-icon) { color:var(--text-secondary); }.summary-card strong { display:block; color:var(--text-primary); }
.instance-card h3 { margin:5px 0 7px; font-size:1.16rem; }
.dashboard-lower { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:22px; max-width:1400px; margin:auto; }
.dashboard-lower .panel { margin:0; max-width:none; }
.panel-empty { color:var(--text-secondary); margin:0; padding:26px 0; }
.activity-list { display:grid; gap:14px; list-style:none; margin:0; padding:0; }
.activity-list li { display:grid; grid-template-columns:32px minmax(0,1fr) auto; align-items:center; gap:11px; padding-bottom:14px; border-bottom:1px solid #262d47; }
.activity-list li:last-child { border:0; padding-bottom:0; }.activity-icon { display:grid; place-items:center; width:30px; height:30px; color:var(--accent-cyan); background:#102237; border:1px solid #245574; border-radius:8px; }.activity-list strong,.activity-list span { display:block; }.activity-list div span,.activity-list time { color:var(--text-secondary); font-size:.78rem; }.activity-list time { white-space:nowrap; }
.system-list { display:grid; gap:0; margin:0; }.system-list > div { display:flex; align-items:center; justify-content:space-between; gap:16px; min-height:44px; border-bottom:1px solid #262d47; }.system-list > div:last-child { border:0; }.system-list dt { color:var(--text-secondary); }.system-list dd { display:flex; align-items:center; justify-content:end; gap:8px; margin:0; text-align:right; }.system-list .is-good::before,.system-list .is-bad::before { content:""; width:8px; height:8px; border-radius:50%; background:currentColor; box-shadow:0 0 10px currentColor; }.system-list .is-good { color:var(--status-running); }.system-list .is-bad { color:var(--status-error); }.storage-track { display:block; width:90px; height:6px; overflow:hidden; border-radius:99px; background:#272e49; }.storage-track i { display:block; height:100%; border-radius:inherit; background:linear-gradient(90deg,var(--accent-cyan),var(--accent-magenta)); }
@media (max-width: 1050px) { .summary-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } .dashboard-lower { grid-template-columns:1fr; } }
@media (max-width: 600px) { .summary-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } .summary-card:last-child { grid-column:span 2; } .activity-list li { grid-template-columns:32px minmax(0,1fr); } .activity-list time { grid-column:2; } .system-list > div { align-items:flex-start; flex-direction:column; padding:10px 0; gap:3px; } .system-list dd { justify-content:start; text-align:left; } }
+150 -1
View File
@@ -1 +1,150 @@
{{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}}
{{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}}
+245 -1
View File
@@ -1 +1,245 @@
{{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}}
{{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}}
+108 -1
View File
@@ -1 +1,108 @@
{{define "catalog-detail.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.CatalogDetail.Game.Name}} · 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"><a class="back-link" href="/catalog">← {{.Msg "catalog.back"}}</a><section class="game-detail"><aside><div class="game-cover"><img src="{{.CatalogDetail.Game.Image}}" alt="{{.CatalogDetail.Game.Name}}"><span>◈</span></div><div class="deploy-band">{{if .IsAdmin}}<a class="link-button" href="/catalog/{{.CatalogDetail.ID}}/deploy">{{.Msg "catalog.deploy"}}</a>{{end}}</div></aside><article><p class="eyebrow">{{.Msg "catalog.eyebrow"}}</p><h1>{{.CatalogDetail.Game.Name}}</h1><p class="game-description">{{.CatalogDetail.Game.Description}}</p><div class="requirements-grid"><section><h2>{{.Msg "catalog.minimum"}}</h2><dl><dt>CPU</dt><dd>{{.CatalogDetail.Requirements.Minimum.CPUCores}}</dd><dt>{{.Msg "catalog.memory"}}</dt><dd>{{.CatalogDetail.Requirements.Minimum.MemoryMB}} MB</dd><dt>{{.Msg "catalog.storage"}}</dt><dd>{{.CatalogDetail.Requirements.Minimum.StorageGB}} GB</dd></dl></section><section><h2>{{.Msg "catalog.recommended"}}</h2><dl><dt>CPU</dt><dd>{{.CatalogDetail.Requirements.Recommended.CPUCores}}</dd><dt>{{.Msg "catalog.memory"}}</dt><dd>{{.CatalogDetail.Requirements.Recommended.MemoryMB}} MB</dd><dt>{{.Msg "catalog.storage"}}</dt><dd>{{.CatalogDetail.Requirements.Recommended.StorageGB}} GB</dd></dl></section></div></article></section></main></body></html>{{end}}
{{define "catalog-detail.html"}}
<!doctype html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
{{.CatalogDetail.Game.Name}}
· 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">
<a class="back-link" href="/catalog">
←
{{.Msg "catalog.back"}}
</a>
<section class="game-detail">
<aside>
<div class="game-cover">
<img src="{{.CatalogDetail.Game.Image}}" alt="{{.CatalogDetail.Game.Name}}">
<span>
◈
</span>
</div>
<div class="deploy-band">
{{if .IsAdmin}}
<a class="link-button" href="/catalog/{{.CatalogDetail.ID}}/deploy">
{{.Msg "catalog.deploy"}}
</a>
{{end}}
</div>
</aside>
<article>
<p class="eyebrow">
{{.Msg "catalog.eyebrow"}}
</p>
<h1>
{{.CatalogDetail.Game.Name}}
</h1>
<p class="game-description">
{{.CatalogDetail.Game.Description}}
</p>
<div class="requirements-grid">
<section>
<h2>
{{.Msg "catalog.minimum"}}
</h2>
<dl>
<dt>
CPU
</dt>
<dd>
{{.CatalogDetail.Requirements.Minimum.CPUCores}}
</dd>
<dt>
{{.Msg "catalog.memory"}}
</dt>
<dd>
{{.CatalogDetail.Requirements.Minimum.MemoryMB}}
MB
</dd>
<dt>
{{.Msg "catalog.storage"}}
</dt>
<dd>
{{.CatalogDetail.Requirements.Minimum.StorageGB}}
GB
</dd>
</dl>
</section>
<section>
<h2>
{{.Msg "catalog.recommended"}}
</h2>
<dl>
<dt>
CPU
</dt>
<dd>
{{.CatalogDetail.Requirements.Recommended.CPUCores}}
</dd>
<dt>
{{.Msg "catalog.memory"}}
</dt>
<dd>
{{.CatalogDetail.Requirements.Recommended.MemoryMB}}
MB
</dd>
<dt>
{{.Msg "catalog.storage"}}
</dt>
<dd>
{{.CatalogDetail.Requirements.Recommended.StorageGB}}
GB
</dd>
</dl>
</section>
</div>
</article>
</section>
</main>
</body>
</html>
{{end}}
+113 -1
View File
@@ -1 +1,113 @@
{{define "catalog.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 catalog-header"><div><p class="eyebrow">{{.Msg "catalog.eyebrow"}}</p><h1>{{.Msg "catalog.heading"}}</h1></div><div class="catalog-tools"><label class="instance-search"><span class="sr-only">{{.Msg "catalog.search"}}</span><input id="catalog-search" type="search" placeholder="{{.Msg "catalog.search_placeholder"}}" autocomplete="off"><kbd>/</kbd></label>{{if and .IsAdmin .CatalogScanner}}<form method="post" action="/catalog/scan"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><button class="scan-button" type="submit">↻ {{.Msg "catalog.scan"}}</button></form>{{end}}</div></header>{{if .CatalogScan}}<section class="catalog-scan-result"><strong>{{.CatalogScan.Found}} {{.Msg "catalog.found"}} · {{len .CatalogScan.Valid}} {{.Msg "catalog.valid"}} · {{len .CatalogScan.Errors}} {{.Msg "catalog.invalid"}}</strong>{{if .CatalogScan.Errors}}<ul>{{range .CatalogScan.Errors}}<li><strong>{{.Template}}</strong> — {{.Message}}</li>{{end}}</ul>{{end}}</section>{{end}}{{if .Catalog}}<section class="catalog-grid" aria-label="{{.Msg "catalog.heading"}}">{{range .Catalog}}<a class="catalog-card" data-catalog-card data-search="{{.GameName}} {{.ID}} {{.Description}}" href="/catalog/{{.ID}}"><div class="catalog-cover"><img src="{{.Image}}" alt="{{.GameName}}" loading="lazy"><span class="cover-fallback">◈</span></div><h2>{{.GameName}}</h2></a>{{end}}</section><p class="search-empty" data-catalog-search-empty hidden>{{.Msg "catalog.no_match"}}</p>{{else}}<section class="empty-state"><span class="empty-icon">◈</span><h2>{{.Msg "catalog.empty"}}</h2>{{if .IsAdmin}}<p>{{.Msg "catalog.empty_admin"}}</p>{{end}}</section>{{end}}</main><script src="/static/app.v2.js"></script></body></html>{{end}}
{{define "catalog.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 catalog-header">
<div>
<p class="eyebrow">
{{.Msg "catalog.eyebrow"}}
</p>
<h1>
{{.Msg "catalog.heading"}}
</h1>
</div>
<div class="catalog-tools">
<label class="instance-search">
<span class="sr-only">
{{.Msg "catalog.search"}}
</span>
<input id="catalog-search" type="search" placeholder="{{.Msg "catalog.search_placeholder"}}" autocomplete="off">
<kbd>
/
</kbd>
</label>
{{if and .IsAdmin .CatalogScanner}}
<form method="post" action="/catalog/scan">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<button class="scan-button" type="submit">
↻
{{.Msg "catalog.scan"}}
</button>
</form>
{{end}}
</div>
</header>
{{if .CatalogScan}}
<section class="catalog-scan-result">
<strong>
{{.CatalogScan.Found}}
{{.Msg "catalog.found"}}
·
{{len .CatalogScan.Valid}}
{{.Msg "catalog.valid"}}
·
{{len .CatalogScan.Errors}}
{{.Msg "catalog.invalid"}}
</strong>
{{if .CatalogScan.Errors}}
<ul>
{{range .CatalogScan.Errors}}
<li>
<strong>
{{.Template}}
</strong>
—
{{.Message}}
</li>
{{end}}
</ul>
{{end}}
</section>
{{end}}
{{if .Catalog}}
<section class="catalog-grid" aria-label="{{.Msg "catalog.heading"}}">
{{range .Catalog}}
<a class="catalog-card" data-catalog-card data-search="{{.GameName}} {{.ID}} {{.Description}}" href="/catalog/{{.ID}}">
<div class="catalog-cover">
<img src="{{.Image}}" alt="{{.GameName}}" loading="lazy">
<span class="cover-fallback">
◈
</span>
</div>
<h2>
{{.GameName}}
</h2>
</a>
{{end}}
</section>
<p class="search-empty" data-catalog-search-empty hidden>
{{.Msg "catalog.no_match"}}
</p>
{{else}}
<section class="empty-state">
<span class="empty-icon">
◈
</span>
<h2>
{{.Msg "catalog.empty"}}
</h2>
{{if .IsAdmin}}
<p>
{{.Msg "catalog.empty_admin"}}
</p>
{{end}}
</section>
{{end}}
</main>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}
+57 -20
View File
@@ -1,22 +1,59 @@
{{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>
<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}}
+108 -1
View File
@@ -1 +1,108 @@
{{define "deployment.html"}}<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{.Msg "deployment.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"><a class="back-link" href="/catalog/{{.Deployment.Template.ID}}">← {{.Msg "catalog.back"}}</a><section class="deployment-layout"><aside class="panel deployment-game"><img src="{{.Deployment.Template.Game.Image}}" alt="{{.Deployment.Template.Game.Name}}"><h1>{{.Deployment.Template.Game.Name}}</h1><p>{{.Deployment.Template.Game.Description}}</p></aside><form class="panel deployment-form" method="post" enctype="multipart/form-data" data-deployment-form><input type="hidden" name="csrf_token" value="{{.CSRFToken}}">{{if .Deployment.Error}}<p class="error">{{.Deployment.Error}}</p>{{end}}<p class="eyebrow">{{.Msg "deployment.eyebrow"}}</p><h1>{{.Msg "deployment.heading"}}</h1><label>{{.Msg "deployment.name"}}<input required maxlength="100" name="display_name" autocomplete="off"></label><label>{{.Msg "deployment.description"}}<textarea name="description" rows="3"></textarea></label><h2>{{.Msg "deployment.parameters"}}</h2>{{range .Deployment.Template.Configuration.Fields}}<label>{{.Label}}{{if .Required}} *{{end}}{{if .Description}}<small class="help">{{.Description}}</small>{{end}}{{if eq .Type "boolean"}}<select name="config_{{.ID}}"><option value="true"{{if eq (index $.Deployment.Values .ID) "true"}} selected{{end}}>true</option><option value="false"{{if eq (index $.Deployment.Values .ID) "false"}} selected{{end}}>false</option></select>{{else if eq .Type "enum"}}<select name="config_{{.ID}}">{{range .Values}}<option value="{{.}}">{{.}}</option>{{end}}</select>{{else}}<input name="config_{{.ID}}" type="{{if eq .Type "secret"}}password{{else if or (eq .Type "integer") (eq .Type "number")}}number{{else}}text{{end}}" value="{{if ne .Type "secret"}}{{index $.Deployment.Values .ID}}{{end}}" {{if .Required}}required{{end}}>{{end}}</label>{{end}}{{if .Deployment.Template.Imports.Supported}}<label>{{.Msg "deployment.backup"}}<input name="backup" type="file" accept=".zip,.tar,.tar.gz,.tar.zst"></label><p class="help">{{.Msg "deployment.backup_help"}}</p>{{end}}<button type="submit" data-deployment-submit>{{.Msg "deployment.go"}}</button></form></section></main><script src="/static/app.v2.js"></script></body></html>{{end}}
{{define "deployment.html"}}
<!doctype html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
{{.Msg "deployment.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">
<a class="back-link" href="/catalog/{{.Deployment.Template.ID}}">
←
{{.Msg "catalog.back"}}
</a>
<section class="deployment-layout">
<aside class="panel deployment-game">
<img src="{{.Deployment.Template.Game.Image}}" alt="{{.Deployment.Template.Game.Name}}">
<h1>
{{.Deployment.Template.Game.Name}}
</h1>
<p>
{{.Deployment.Template.Game.Description}}
</p>
</aside>
<form class="panel deployment-form" method="post" enctype="multipart/form-data" data-deployment-form>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
{{if .Deployment.Error}}
<p class="error">
{{.Deployment.Error}}
</p>
{{end}}
<p class="eyebrow">
{{.Msg "deployment.eyebrow"}}
</p>
<h1>
{{.Msg "deployment.heading"}}
</h1>
<label>
{{.Msg "deployment.name"}}
<input required maxlength="100" name="display_name" autocomplete="off">
</label>
<label>
{{.Msg "deployment.description"}}
<textarea name="description" rows="3">
</textarea>
</label>
<h2>
{{.Msg "deployment.parameters"}}
</h2>
{{range .Deployment.Template.Configuration.Fields}}
<label>
{{.Label}}
{{if .Required}}
*
{{end}}
{{if .Description}}
<small class="help">
{{.Description}}
</small>
{{end}}
{{if eq .Type "boolean"}}
<select name="config_{{.ID}}">
<option value="true"{{if eq (index $.Deployment.Values .ID) "true"}} selected{{end}}>
true
</option>
<option value="false"{{if eq (index $.Deployment.Values .ID) "false"}} selected{{end}}>
false
</option>
</select>
{{else if eq .Type "enum"}}
<select name="config_{{.ID}}">
{{range .Values}}
<option value="{{.}}">
{{.}}
</option>
{{end}}
</select>
{{else}}
<input name="config_{{.ID}}" type="{{if eq .Type "secret"}}password{{else if or (eq .Type "integer") (eq .Type "number")}}number{{else}}text{{end}}" value="{{if ne .Type "secret"}}{{index $.Deployment.Values .ID}}{{end}}" {{if .Required}}required{{end}}>
{{end}}
</label>
{{end}}
{{if .Deployment.Template.Imports.Supported}}
<label>
{{.Msg "deployment.backup"}}
<input name="backup" type="file" accept=".zip,.tar,.tar.gz,.tar.zst">
</label>
<p class="help">
{{.Msg "deployment.backup_help"}}
</p>
{{end}}
<button type="submit" data-deployment-submit>
{{.Msg "deployment.go"}}
</button>
</form>
</section>
</main>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}
+223 -1
View File
@@ -1 +1,223 @@
{{define "home.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 dashboard-header"><div><p class="eyebrow">{{.Msg "dashboard.eyebrow"}}</p><h1>{{.Msg "dashboard.heading"}}</h1><p>{{.Msg "dashboard.introduction"}}</p></div><label class="instance-search"><span class="sr-only">{{.Msg "dashboard.search"}}</span><input id="instance-search" type="search" placeholder="{{.Msg "dashboard.search_placeholder"}}" autocomplete="off"><kbd>/</kbd></label></header><section class="summary-grid" aria-label="{{.Msg "dashboard.summary"}}"><article class="summary-card total"><span>{{.Msg "dashboard.total"}}</span><strong>{{len .Instances}}</strong></article><article class="summary-card running"><span>{{.Msg "dashboard.running"}}</span><strong>{{index .StatusCounts "running"}}</strong></article><article class="summary-card stopped"><span>{{.Msg "dashboard.stopped"}}</span><strong>{{index .StatusCounts "stopped"}}</strong></article><article class="summary-card updating"><span>{{.Msg "dashboard.updating"}}</span><strong>{{index .StatusCounts "updating"}}</strong></article><article class="summary-card error"><span>{{.Msg "dashboard.error"}}</span><strong>{{index .StatusCounts "error"}}</strong></article></section><section class="dashboard-section" aria-labelledby="instances-heading"><div class="section-heading"><div><p class="eyebrow">{{.Msg "dashboard.instances_eyebrow"}}</p><h2 id="instances-heading">{{.Msg "dashboard.instances"}}</h2></div></div>{{if .Instances}}<div class="instance-grid">{{range .Instances}}<a class="instance-card" href="/instances/{{.ID}}" data-instance-card data-search="{{.Preview.Game.Name}} {{.Preview.DisplayName}}"><div class="instance-art">{{if .Preview.Game.ArtworkURL}}<img src="{{.Preview.Game.ArtworkURL}}" alt="">{{else}}<span>◈</span>{{end}}<span class="status-badge status-{{statusClass .LifecycleState}}"><i></i>{{statusLabel .LifecycleState}}</span></div><div class="instance-card-body"><p class="game-name">{{.Preview.Game.Name}}</p><h3>{{.Preview.DisplayName}}</h3><p class="instance-id">{{.ID}}</p></div></a>{{end}}</div><p class="search-empty" data-search-empty hidden>{{.Msg "dashboard.no_match"}}</p>{{else}}<div class="empty-state"><span class="empty-icon">◈</span><h2>{{.Msg "dashboard.empty_heading"}}</h2><p>{{.Msg "dashboard.empty_copy"}}</p></div>{{end}}</section><div class="instance-detail-lower"><section class="panel" aria-labelledby="activity-heading"><div class="section-heading"><div><p class="eyebrow">{{.Msg "dashboard.activity_eyebrow"}}</p><h2 id="activity-heading">{{.Msg "dashboard.recent_activity"}}</h2></div></div>{{if .RecentActivity}}<ul>{{range .RecentActivity}}<li>{{activityLabel .Action}} · {{.Instance}} · {{.Actor}}</li>{{end}}</ul>{{else}}<p class="muted">{{.Msg "dashboard.no_activity"}}</p>{{end}}</section><section class="panel" aria-labelledby="system-heading"><div class="section-heading"><div><p class="eyebrow">{{.Msg "dashboard.system_eyebrow"}}</p><h2 id="system-heading">{{.Msg "dashboard.system_status"}}</h2></div></div><ul><li>{{.Msg "dashboard.agent"}}: {{if .SystemStatus.AgentAvailable}}{{if .SystemStatus.AgentOnline}}{{.Msg "dashboard.online"}}{{else}}{{.Msg "dashboard.offline"}}{{end}}{{else}}{{.Msg "dashboard.unavailable"}}{{end}}</li><li>{{.Msg "dashboard.database"}}: {{if .SystemStatus.DatabaseHealthy}}{{.Msg "dashboard.healthy"}}{{else}}{{.Msg "dashboard.unavailable"}}{{end}}</li><li>{{.Msg "dashboard.storage"}}: {{if .SystemStatus.StorageAvailable}}{{storagePercent .SystemStatus.StorageUsed .SystemStatus.StorageTotal}}%{{else}}{{.Msg "dashboard.unavailable"}}{{end}}</li></ul></section></div></main><script src="/static/app.v2.js"></script></body></html>{{end}}
{{define "home.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 dashboard-header">
<div>
<p class="eyebrow">
{{.Msg "dashboard.eyebrow"}}
</p>
<h1>
{{.Msg "dashboard.heading"}}
</h1>
<p>
{{.Msg "dashboard.introduction"}}
</p>
</div>
<label class="instance-search">
<span class="sr-only">
{{.Msg "dashboard.search"}}
</span>
<input id="instance-search" type="search" placeholder="{{.Msg "dashboard.search_placeholder"}}" autocomplete="off">
<kbd>
/
</kbd>
</label>
</header>
<section class="summary-grid" aria-label="{{.Msg "dashboard.summary"}}">
<article class="summary-card total">
<span>
{{.Msg "dashboard.total"}}
</span>
<strong>
{{len .Instances}}
</strong>
</article>
<article class="summary-card running">
<span>
{{.Msg "dashboard.running"}}
</span>
<strong>
{{index .StatusCounts "running"}}
</strong>
</article>
<article class="summary-card stopped">
<span>
{{.Msg "dashboard.stopped"}}
</span>
<strong>
{{index .StatusCounts "stopped"}}
</strong>
</article>
<article class="summary-card updating">
<span>
{{.Msg "dashboard.updating"}}
</span>
<strong>
{{index .StatusCounts "updating"}}
</strong>
</article>
<article class="summary-card error">
<span>
{{.Msg "dashboard.error"}}
</span>
<strong>
{{index .StatusCounts "error"}}
</strong>
</article>
</section>
<section class="dashboard-section" aria-labelledby="instances-heading">
<div class="section-heading">
<div>
<p class="eyebrow">
{{.Msg "dashboard.instances_eyebrow"}}
</p>
<h2 id="instances-heading">
{{.Msg "dashboard.instances"}}
</h2>
</div>
</div>
{{if .Instances}}
<div class="instance-grid">
{{range .Instances}}
<a class="instance-card" href="/instances/{{.ID}}" data-instance-card data-search="{{.Preview.Game.Name}} {{.Preview.DisplayName}}">
<div class="instance-art">
{{if .Preview.Game.ArtworkURL}}
<img src="{{.Preview.Game.ArtworkURL}}" alt="">
{{else}}
<span>
◈
</span>
{{end}}
<span class="status-badge status-{{statusClass .LifecycleState}}">
<i>
</i>
{{statusLabel .LifecycleState}}
</span>
</div>
<div class="instance-card-body">
<p class="game-name">
{{.Preview.Game.Name}}
</p>
<h3>
{{.Preview.DisplayName}}
</h3>
<p class="instance-id">
{{.ID}}
</p>
</div>
</a>
{{end}}
</div>
<p class="search-empty" data-search-empty hidden>
{{.Msg "dashboard.no_match"}}
</p>
{{else}}
<div class="empty-state">
<span class="empty-icon">
◈
</span>
<h2>
{{.Msg "dashboard.empty_heading"}}
</h2>
<p>
{{.Msg "dashboard.empty_copy"}}
</p>
</div>
{{end}}
</section>
<div class="instance-detail-lower">
<section class="panel" aria-labelledby="activity-heading">
<div class="section-heading">
<div>
<p class="eyebrow">
{{.Msg "dashboard.activity_eyebrow"}}
</p>
<h2 id="activity-heading">
{{.Msg "dashboard.recent_activity"}}
</h2>
</div>
</div>
{{if .RecentActivity}}
<ul>
{{range .RecentActivity}}
<li>
{{activityLabel .Action}}
·
{{.Instance}}
·
{{.Actor}}
</li>
{{end}}
</ul>
{{else}}
<p class="muted">
{{.Msg "dashboard.no_activity"}}
</p>
{{end}}
</section>
<section class="panel" aria-labelledby="system-heading">
<div class="section-heading">
<div>
<p class="eyebrow">
{{.Msg "dashboard.system_eyebrow"}}
</p>
<h2 id="system-heading">
{{.Msg "dashboard.system_status"}}
</h2>
</div>
</div>
<ul>
<li>
{{.Msg "dashboard.agent"}}
:
{{if .SystemStatus.AgentAvailable}}
{{if .SystemStatus.AgentOnline}}
{{.Msg "dashboard.online"}}
{{else}}
{{.Msg "dashboard.offline"}}
{{end}}
{{else}}
{{.Msg "dashboard.unavailable"}}
{{end}}
</li>
<li>
{{.Msg "dashboard.database"}}
:
{{if .SystemStatus.DatabaseHealthy}}
{{.Msg "dashboard.healthy"}}
{{else}}
{{.Msg "dashboard.unavailable"}}
{{end}}
</li>
<li>
{{.Msg "dashboard.storage"}}
:
{{if .SystemStatus.StorageAvailable}}
{{storagePercent .SystemStatus.StorageUsed .SystemStatus.StorageTotal}}
%
{{else}}
{{.Msg "dashboard.unavailable"}}
{{end}}
</li>
</ul>
</section>
</div>
</main>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}
File diff suppressed because one or more lines are too long
+51 -1
View File
@@ -1 +1,51 @@
{{define "login.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.v1.css"></head><body class="auth-body"><main class="auth-card"><div class="auth-brand"><img src="/static/dogama-brand-banner.png" alt="DoGaMa"></div><div class="auth-content"><p class="eyebrow">{{.Msg "brand.control"}}</p><h1>{{.Msg "login.heading"}}</h1><p class="auth-copy">{{.Msg "brand.control"}}</p>{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}<form method="post" action="/login"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "field.username"}}<input name="username" autocomplete="username" required></label><label>{{.Msg "field.password"}}<input type="password" name="password" autocomplete="current-password" required></label><button type="submit">{{.Msg "login.submit"}}</button></form></div></main></body></html>{{end}}
{{define "login.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.v1.css">
</head>
<body class="auth-body">
<main class="auth-card">
<div class="auth-brand">
<img src="/static/dogama-brand-banner.png" alt="DoGaMa">
</div>
<div class="auth-content">
<p class="eyebrow">
{{.Msg "brand.control"}}
</p>
<h1>
{{.Msg "login.heading"}}
</h1>
<p class="auth-copy">
{{.Msg "brand.control"}}
</p>
{{if .Error}}
<p class="error" role="alert">
{{.Error}}
</p>
{{end}}
<form method="post" action="/login">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label>
{{.Msg "field.username"}}
<input name="username" autocomplete="username" required>
</label>
<label>
{{.Msg "field.password"}}
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit">
{{.Msg "login.submit"}}
</button>
</form>
</div>
</main>
</body>
</html>
{{end}}
+351 -18
View File
@@ -1,20 +1,353 @@
{{define "settings.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.v1.css"></head>
<body class="app-body">{{template "sidebar" .}}
<main class="app-main">
<div class="page-heading"><div><p class="eyebrow">{{.Msg "settings.eyebrow"}}</p><h1>Administration</h1><p>{{.Msg "settings.introduction"}}</p></div></div>
<nav class="tabs" aria-label="{{.Msg "settings.tabs"}}"><a href="/administration/users">{{.Msg "template_repositories.users"}}</a><a href="#web-access">{{.Msg "settings.web_access"}}</a><a href="#notifications">{{.Msg "settings.notifications"}}</a><a href="/administration/template-repositories">{{.Msg "template_repositories.heading"}}</a><a href="#audit">{{.Msg "settings.audit"}}</a><a href="#containers">{{.Msg "settings.containers"}}</a></nav>
<section class="panel settings-section" id="web-access"><h2>{{.Msg "settings.web_access"}}</h2><p>{{.Msg "settings.web_help"}}</p><form method="post" action="/admin/web-access"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label class="check"><input type="checkbox" name="require_https"{{if .RequireHTTPS}} checked{{end}}> {{.Msg "settings.require_https"}}</label><p class="help">{{.Msg "settings.https_help"}}</p><label>{{.Msg "settings.canonical_url"}}<input name="canonical_url" type="url" value="{{.CanonicalURL}}" placeholder="https://dogama.example.com"></label><p class="help">{{.Msg "settings.canonical_help"}}</p><button type="submit">{{.Msg "settings.save_web"}}</button></form></section>
<section class="panel settings-section" id="notifications"><h2>{{.Msg "settings.channels"}}</h2><p>{{.Msg "settings.channels_help"}}</p><div class="tabs"><a href="#notification-general">{{.Msg "settings.notification_general"}}</a><a href="#email">{{.Msg "settings.notification_email"}}</a><a href="#discord">{{.Msg "settings.notification_discord"}}</a><a href="#gotify">{{.Msg "settings.notification_gotify"}}</a></div>
<section id="notification-general"><h3>{{.Msg "settings.notification_general"}}</h3><p>{{.Msg "settings.notification_language_help"}}</p><form method="post" action="/admin/notification-language"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "language"}}<select name="language">{{range .Languages}}<option value="{{.Code}}"{{if eq $.NotificationLanguage .Code}} selected{{end}}>{{.Display}}</option>{{end}}</select></label><button type="submit">{{.Msg "language.save"}}</button></form></section>
<ul class="channel-list">{{range .Channels}}<li><strong>{{.Name}}</strong><small>{{.Type}} · {{if .Enabled}}{{$.Msg "settings.enabled"}}{{else}}{{$.Msg "settings.disabled"}}{{end}} · {{if .Configured}}{{$.Msg "settings.configured"}}{{else}}{{$.Msg "settings.not_configured"}}{{end}}</small><form method="post" action="/admin/notification-channels/{{.ID}}/test"><input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"><button type="submit">{{$.Msg "settings.send_test"}}</button></form></li>{{else}}<li>{{.Msg "settings.no_channels"}}</li>{{end}}</ul>
<section id="email"><h3>{{.Msg "settings.notification_email"}}</h3><form method="post" action="/admin/notification-channels"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><input type="hidden" name="name" value="SMTP email"><input type="hidden" name="type" value="email"><input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed start.completed stop.completed"><label class="check"><input type="checkbox" name="enabled" checked> {{.Msg "settings.enabled"}}</label><div class="form-grid"><label>{{.Msg "settings.sender_name"}}<input name="from_name"></label><label>{{.Msg "settings.sender_email"}}<input required type="email" name="from"></label><label>{{.Msg "settings.smtp_server"}}<input required name="host"></label><label>{{.Msg "settings.port"}}<input required name="port" type="number" min="1" max="65535" value="587"></label><label>{{.Msg "settings.username"}}<input name="smtp_username"></label><label>{{.Msg "settings.password"}} <small>{{.Msg "settings.password_retain"}}</small><input name="smtp_password" type="password"></label><label>{{.Msg "settings.tls_mode"}}<select name="tls_mode"><option value="starttls">STARTTLS</option><option value="tls">Direct TLS / SMTPS</option><option value="none">No TLS</option></select></label><label>{{.Msg "settings.test_recipient"}}<input type="email" name="to"></label></div><button type="submit">{{.Msg "settings.save_smtp"}}</button></form></section>
<section id="discord"><h3>{{.Msg "settings.notification_discord"}}</h3><form method="post" action="/admin/notification-channels"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><input type="hidden" name="name" value="Discord"><input type="hidden" name="type" value="discord"><input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed"><label class="check"><input type="checkbox" name="enabled" checked> {{.Msg "settings.enabled"}}</label><label>{{.Msg "settings.webhook_url"}}<input required type="url" name="url" placeholder="https://discord.com/api/webhooks/…"></label><button type="submit">{{.Msg "settings.save_discord"}}</button></form></section>
<section id="gotify"><h3>{{.Msg "settings.notification_gotify"}}</h3><p>{{.Msg "settings.gotify_help"}}</p><form method="post" action="/admin/notification-channels"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><input type="hidden" name="name" value="Gotify"><input type="hidden" name="type" value="gotify"><input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed"><label class="check"><input type="checkbox" name="enabled" checked> {{.Msg "settings.enabled"}}</label><label>{{.Msg "settings.server_url"}}<input required type="url" name="url" placeholder="https://gotify.example"></label><label>{{.Msg "settings.application_token"}}<input required name="gotify_token" type="password"></label><button type="submit">{{.Msg "settings.save_gotify"}}</button></form></section>
</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><script src="/static/app.v2.js"></script></body></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.v1.css">
</head>
<body class="app-body">
{{template "sidebar" .}}
<main class="app-main">
<div class="page-heading">
<div>
<p class="eyebrow">
{{.Msg "settings.eyebrow"}}
</p>
<h1>
Administration
</h1>
<p>
{{.Msg "settings.introduction"}}
</p>
</div>
</div>
<nav class="tabs" aria-label="{{.Msg "settings.tabs"}}">
<a href="/administration/users">
{{.Msg "template_repositories.users"}}
</a>
<a href="#web-access">
{{.Msg "settings.web_access"}}
</a>
<a href="#notifications">
{{.Msg "settings.notifications"}}
</a>
<a href="/administration/template-repositories">
{{.Msg "template_repositories.heading"}}
</a>
<a href="#audit">
{{.Msg "settings.audit"}}
</a>
<a href="#containers">
{{.Msg "settings.containers"}}
</a>
</nav>
<section class="panel settings-section" id="web-access">
<h2>
{{.Msg "settings.web_access"}}
</h2>
<p>
{{.Msg "settings.web_help"}}
</p>
<form method="post" action="/admin/web-access">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label class="check">
<input type="checkbox" name="require_https"{{if .RequireHTTPS}} checked{{end}}>
{{.Msg "settings.require_https"}}
</label>
<p class="help">
{{.Msg "settings.https_help"}}
</p>
<label>
{{.Msg "settings.canonical_url"}}
<input name="canonical_url" type="url" value="{{.CanonicalURL}}" placeholder="https://dogama.example.com">
</label>
<p class="help">
{{.Msg "settings.canonical_help"}}
</p>
<button type="submit">
{{.Msg "settings.save_web"}}
</button>
</form>
</section>
<section class="panel settings-section" id="notifications">
<h2>
{{.Msg "settings.channels"}}
</h2>
<p>
{{.Msg "settings.channels_help"}}
</p>
<div class="tabs">
<a href="#notification-general">
{{.Msg "settings.notification_general"}}
</a>
<a href="#email">
{{.Msg "settings.notification_email"}}
</a>
<a href="#discord">
{{.Msg "settings.notification_discord"}}
</a>
<a href="#gotify">
{{.Msg "settings.notification_gotify"}}
</a>
</div>
<section id="notification-general">
<h3>
{{.Msg "settings.notification_general"}}
</h3>
<p>
{{.Msg "settings.notification_language_help"}}
</p>
<form method="post" action="/admin/notification-language">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label>
{{.Msg "language"}}
<select name="language">
{{range .Languages}}
<option value="{{.Code}}"{{if eq $.NotificationLanguage .Code}} selected{{end}}>
{{.Display}}
</option>
{{end}}
</select>
</label>
<button type="submit">
{{.Msg "language.save"}}
</button>
</form>
</section>
<ul class="channel-list">
{{range .Channels}}
<li>
<strong>
{{.Name}}
</strong>
<small>
{{.Type}}
·
{{if .Enabled}}
{{$.Msg "settings.enabled"}}
{{else}}
{{$.Msg "settings.disabled"}}
{{end}}
·
{{if .Configured}}
{{$.Msg "settings.configured"}}
{{else}}
{{$.Msg "settings.not_configured"}}
{{end}}
</small>
<form method="post" action="/admin/notification-channels/{{.ID}}/test">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button type="submit">
{{$.Msg "settings.send_test"}}
</button>
</form>
</li>
{{else}}
<li>
{{.Msg "settings.no_channels"}}
</li>
{{end}}
</ul>
<section id="email">
<h3>
{{.Msg "settings.notification_email"}}
</h3>
<form method="post" action="/admin/notification-channels">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="name" value="SMTP email">
<input type="hidden" name="type" value="email">
<input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed start.completed stop.completed">
<label class="check">
<input type="checkbox" name="enabled" checked>
{{.Msg "settings.enabled"}}
</label>
<div class="form-grid">
<label>
{{.Msg "settings.sender_name"}}
<input name="from_name">
</label>
<label>
{{.Msg "settings.sender_email"}}
<input required type="email" name="from">
</label>
<label>
{{.Msg "settings.smtp_server"}}
<input required name="host">
</label>
<label>
{{.Msg "settings.port"}}
<input required name="port" type="number" min="1" max="65535" value="587">
</label>
<label>
{{.Msg "settings.username"}}
<input name="smtp_username">
</label>
<label>
{{.Msg "settings.password"}}
<small>
{{.Msg "settings.password_retain"}}
</small>
<input name="smtp_password" type="password">
</label>
<label>
{{.Msg "settings.tls_mode"}}
<select name="tls_mode">
<option value="starttls">
STARTTLS
</option>
<option value="tls">
Direct TLS / SMTPS
</option>
<option value="none">
No TLS
</option>
</select>
</label>
<label>
{{.Msg "settings.test_recipient"}}
<input type="email" name="to">
</label>
</div>
<button type="submit">
{{.Msg "settings.save_smtp"}}
</button>
</form>
</section>
<section id="discord">
<h3>
{{.Msg "settings.notification_discord"}}
</h3>
<form method="post" action="/admin/notification-channels">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="name" value="Discord">
<input type="hidden" name="type" value="discord">
<input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed">
<label class="check">
<input type="checkbox" name="enabled" checked>
{{.Msg "settings.enabled"}}
</label>
<label>
{{.Msg "settings.webhook_url"}}
<input required type="url" name="url" placeholder="https://discord.com/api/webhooks/…">
</label>
<button type="submit">
{{.Msg "settings.save_discord"}}
</button>
</form>
</section>
<section id="gotify">
<h3>
{{.Msg "settings.notification_gotify"}}
</h3>
<p>
{{.Msg "settings.gotify_help"}}
</p>
<form method="post" action="/admin/notification-channels">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<input type="hidden" name="name" value="Gotify">
<input type="hidden" name="type" value="gotify">
<input type="hidden" name="events" value="backup.completed backup.failed restore.completed restore.failed update.completed update.failed">
<label class="check">
<input type="checkbox" name="enabled" checked>
{{.Msg "settings.enabled"}}
</label>
<label>
{{.Msg "settings.server_url"}}
<input required type="url" name="url" placeholder="https://gotify.example">
</label>
<label>
{{.Msg "settings.application_token"}}
<input required name="gotify_token" type="password">
</label>
<button type="submit">
{{.Msg "settings.save_gotify"}}
</button>
</form>
</section>
</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>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}
+65 -1
View File
@@ -1 +1,65 @@
{{define "setup.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.v1.css"></head><body class="auth-body"><main class="auth-card"><div class="auth-brand"><img src="/static/dogama-brand-banner.png" alt="DoGaMa"></div><div class="auth-content"><p class="eyebrow">{{.Msg "brand.control"}}</p><h1>{{.Msg "setup.heading"}}</h1><p class="auth-copy">{{.Msg "setup.introduction"}}</p>{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}<form method="post" action="/setup"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><label>{{.Msg "field.username"}}<input name="username" minlength="3" maxlength="64" autocomplete="username" required></label><label>Email<input type="email" name="email" maxlength="254" autocomplete="email" required></label><label>{{.Msg "field.password"}}<input type="password" name="password" minlength="12" maxlength="1024" autocomplete="new-password" required></label><label>{{.Msg "language"}}<select name="language" required>{{range .Languages}}<option value="{{.Code}}"{{if eq $.Language .Code}} selected{{end}}>{{.Display}}</option>{{end}}</select></label><button type="submit">{{.Msg "setup.submit"}}</button></form></div></main></body></html>{{end}}
{{define "setup.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.v1.css">
</head>
<body class="auth-body">
<main class="auth-card">
<div class="auth-brand">
<img src="/static/dogama-brand-banner.png" alt="DoGaMa">
</div>
<div class="auth-content">
<p class="eyebrow">
{{.Msg "brand.control"}}
</p>
<h1>
{{.Msg "setup.heading"}}
</h1>
<p class="auth-copy">
{{.Msg "setup.introduction"}}
</p>
{{if .Error}}
<p class="error" role="alert">
{{.Error}}
</p>
{{end}}
<form method="post" action="/setup">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<label>
{{.Msg "field.username"}}
<input name="username" minlength="3" maxlength="64" autocomplete="username" required>
</label>
<label>
Email
<input type="email" name="email" maxlength="254" autocomplete="email" required>
</label>
<label>
{{.Msg "field.password"}}
<input type="password" name="password" minlength="12" maxlength="1024" autocomplete="new-password" required>
</label>
<label>
{{.Msg "language"}}
<select name="language" required>
{{range .Languages}}
<option value="{{.Code}}"{{if eq $.Language .Code}} selected{{end}}>
{{.Display}}
</option>
{{end}}
</select>
</label>
<button type="submit">
{{.Msg "setup.submit"}}
</button>
</form>
</div>
</main>
</body>
</html>
{{end}}
@@ -1,9 +1,114 @@
{{define "template-repositories.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.v1.css"></head>
<body class="app-body">{{template "sidebar" .}}<main class="app-main">
<div class="page-heading"><div><p class="eyebrow">Administration</p><h1>{{.Msg "template_repositories.heading"}}</h1><p>{{.Msg "template_repositories.introduction"}}</p></div><a class="button-secondary link-button" href="/administration">Administration</a></div>
<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><script src="/static/app.v2.js"></script></body></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.v1.css">
</head>
<body class="app-body">
{{template "sidebar" .}}
<main class="app-main">
<div class="page-heading">
<div>
<p class="eyebrow">
Administration
</p>
<h1>
{{.Msg "template_repositories.heading"}}
</h1>
<p>
{{.Msg "template_repositories.introduction"}}
</p>
</div>
<a class="button-secondary link-button" href="/administration">
Administration
</a>
</div>
<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>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}
+209 -23
View File
@@ -1,25 +1,211 @@
{{define "users.html"}}
<!doctype html><html lang="{{.Language}}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Users · 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"><div><p class="eyebrow">Administration</p><h1>Users</h1><p>Create identities, assign global roles and control access to each game server.</p></div><a class="button-secondary link-button" href="/administration">Administration settings</a></div>
<section class="panel settings-section"><h2>Create user</h2><form method="post" action="/administration/users"><input type="hidden" name="csrf_token" value="{{.CSRFToken}}"><div class="form-grid"><label>Username<input name="username" required maxlength="64" autocomplete="off"></label><label>Email<input name="email" type="email" required autocomplete="off"></label><label>Temporary password<input name="password" type="password" required minlength="12" autocomplete="new-password"></label><label>Global role<select name="role"><option value="user">User</option><option value="admin">Administrator</option></select></label></div><label class="check"><input type="checkbox" name="disabled" value="yes"> Create disabled</label><button type="submit">Create user</button></form></section>
{{range .Users}}
{{$user := .}}
<section class="panel settings-section"><div class="page-heading"><div><h2>{{.Username}}</h2><p>{{if .Disabled}}Disabled{{else}}Active{{end}} · {{.Role}}</p></div></div>
<form method="post" action="/administration/users/{{.ID}}"><input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"><div class="form-grid"><label>Email<input name="email" type="email" value="{{.Email}}" required></label><label>Global role<select name="role"><option value="user"{{if eq .Role "user"}} selected{{end}}>User</option><option value="admin"{{if eq .Role "admin"}} selected{{end}}>Administrator</option></select></label></div><label class="check"><input type="checkbox" name="disabled" value="yes"{{if .Disabled}} checked{{end}}> Disabled (active sessions are revoked)</label><button type="submit">Save user</button></form>
{{if eq .Role "user"}}
<h3>Instance access</h3>
{{if $.Instances}}
{{range $.Instances}}
{{$instance := .}}{{$membership := index (index $.UserAccess $user.ID) .ID}}
<details><summary>{{if .Preview.DisplayName}}{{.Preview.DisplayName}}{{else}}{{.ID}}{{end}} — {{if $membership.Role}}{{$membership.Role}}{{else}}No access{{end}}</summary>
<form method="post" action="/administration/users/{{$user.ID}}/memberships/{{.ID}}"><input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"><label>Instance role<select name="role"><option value="none"{{if not $membership.Role}} selected{{end}}>No access</option><option value="user"{{if eq $membership.Role "user"}} selected{{end}}>User</option><option value="manager"{{if eq $membership.Role "manager"}} selected{{end}}>Manager</option></select></label><button type="submit">Save access</button></form>
{{if $membership.Role}}<div class="permission-grid">{{range $.Permissions}}{{$effect := index $membership.Overrides .}}<form method="post" action="/administration/users/{{$user.ID}}/memberships/{{$instance.ID}}/permissions/{{.}}"><input type="hidden" name="csrf_token" value="{{$.CSRFToken}}"><label>{{.}}<select name="effect"><option value="inherit"{{if not $effect}} selected{{end}}>Role default</option><option value="allow"{{if eq $effect "allow"}} selected{{end}}>Allow</option><option value="deny"{{if eq $effect "deny"}} selected{{end}}>Deny</option></select></label><button type="submit">Save</button></form>{{end}}</div>{{end}}
</details>
{{end}}
{{else}}<p>No game-server instance is currently registered.</p>{{end}}
{{else}}<p>Administrators have implicit access to every instance.</p>{{end}}
</section>
{{else}}<section class="panel"><p>No users found.</p></section>{{end}}
</main><script src="/static/app.v2.js"></script></body></html>
<!doctype html>
<html lang="{{.Language}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
Users · 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">
<div>
<p class="eyebrow">
Administration
</p>
<h1>
Users
</h1>
<p>
Create identities, assign global roles and control access to each game server.
</p>
</div>
<a class="button-secondary link-button" href="/administration">
Administration settings
</a>
</div>
<section class="panel settings-section">
<h2>
Create user
</h2>
<form method="post" action="/administration/users">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-grid">
<label>
Username
<input name="username" required maxlength="64" autocomplete="off">
</label>
<label>
Email
<input name="email" type="email" required autocomplete="off">
</label>
<label>
Temporary password
<input name="password" type="password" required minlength="12" autocomplete="new-password">
</label>
<label>
Global role
<select name="role">
<option value="user">
User
</option>
<option value="admin">
Administrator
</option>
</select>
</label>
</div>
<label class="check">
<input type="checkbox" name="disabled" value="yes">
Create disabled
</label>
<button type="submit">
Create user
</button>
</form>
</section>
{{range .Users}}
{{$user := .}}
<section class="panel settings-section">
<div class="page-heading">
<div>
<h2>
{{.Username}}
</h2>
<p>
{{if .Disabled}}
Disabled
{{else}}
Active
{{end}}
·
{{.Role}}
</p>
</div>
</div>
<form method="post" action="/administration/users/{{.ID}}">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<div class="form-grid">
<label>
Email
<input name="email" type="email" value="{{.Email}}" required>
</label>
<label>
Global role
<select name="role">
<option value="user"{{if eq .Role "user"}} selected{{end}}>
User
</option>
<option value="admin"{{if eq .Role "admin"}} selected{{end}}>
Administrator
</option>
</select>
</label>
</div>
<label class="check">
<input type="checkbox" name="disabled" value="yes"{{if .Disabled}} checked{{end}}>
Disabled (active sessions are revoked)
</label>
<button type="submit">
Save user
</button>
</form>
{{if eq .Role "user"}}
<h3>
Instance access
</h3>
{{if $.Instances}}
{{range $.Instances}}
{{$instance := .}}
{{$membership := index (index $.UserAccess $user.ID) .ID}}
<details>
<summary>
{{if .Preview.DisplayName}}
{{.Preview.DisplayName}}
{{else}}
{{.ID}}
{{end}}
—
{{if $membership.Role}}
{{$membership.Role}}
{{else}}
No access
{{end}}
</summary>
<form method="post" action="/administration/users/{{$user.ID}}/memberships/{{.ID}}">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<label>
Instance role
<select name="role">
<option value="none"{{if not $membership.Role}} selected{{end}}>
No access
</option>
<option value="user"{{if eq $membership.Role "user"}} selected{{end}}>
User
</option>
<option value="manager"{{if eq $membership.Role "manager"}} selected{{end}}>
Manager
</option>
</select>
</label>
<button type="submit">
Save access
</button>
</form>
{{if $membership.Role}}
<div class="permission-grid">
{{range $.Permissions}}
{{$effect := index $membership.Overrides .}}
<form method="post" action="/administration/users/{{$user.ID}}/memberships/{{$instance.ID}}/permissions/{{.}}">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<label>
{{.}}
<select name="effect">
<option value="inherit"{{if not $effect}} selected{{end}}>
Role default
</option>
<option value="allow"{{if eq $effect "allow"}} selected{{end}}>
Allow
</option>
<option value="deny"{{if eq $effect "deny"}} selected{{end}}>
Deny
</option>
</select>
</label>
<button type="submit">
Save
</button>
</form>
{{end}}
</div>
{{end}}
</details>
{{end}}
{{else}}
<p>
No game-server instance is currently registered.
</p>
{{end}}
{{else}}
<p>
Administrators have implicit access to every instance.
</p>
{{end}}
</section>
{{else}}
<section class="panel">
<p>
No users found.
</p>
</section>
{{end}}
</main>
<script src="/static/app.v2.js">
</script>
</body>
</html>
{{end}}