Files
Levyraati26_go/templates/admin.html
T
Esa Kataja 0f15ae0bfc Release 2026.08.02-1
SQLite replaces Postgres, and two fixes from using the thing.

- The database is a file under ./storage instead of a second container. Ten
  members never needed a database server, and the driver is pure Go, so the
  build stays CGO_ENABLED=0 and the dependency count is unchanged. One bind
  mount is now the whole backup: no pgdata, no healthcheck-gated depends_on,
  no startup retry loop. Timestamps are UTC text, idle_ttl is seconds, the
  divisive/unified boards carry their own stddev, and foreign keys are on by
  pragma. Tests get a database file each and run without any setup
- Invites are copied, not clicked. An invite is something to send, and the
  anchor opened the join form in the admin's own browser
- Feedback asks for more than faults: the footer reads "Ongelmia? Ideoita?
  Palautetta?" and the page behind it invites ideas rather than only bugs
- Kuuntele YouTubessa opens in a new tab, so a half-typed review survives it
2026-08-02 20:58:52 +03:00

119 lines
4.3 KiB
HTML

{{define "content"}}
<h1>Ylläpito</h1>
<p><a href="/admin/reports">Palautteet</a>{{if .Data.OpenCount}} <span class="badge pending">{{.Data.OpenCount}} avointa</span>{{end}}</p>
<section class="adminsection">
<header>
<h2>Kutsut</h2>
<form method="post" action="/admin/invites"><button type="submit">Luo kutsukoodi</button></form>
</header>
<div class="body">
<table>
<thead><tr><th>Kutsulinkki</th><th>Tila</th><th>Luotu</th></tr></thead>
<tbody>
{{range .Data.Invites}}
<tr>
<!-- Not a link: an invite is something to send, never to follow. A click used to open the
join form in the admin's own browser, which is never what was wanted. -->
<td class="invitecell">
<code>{{.Link}}</code>
<button type="button" class="ghost" onclick="copyInvite(this)">Kopioi</button>
</td>
<td class="nowrap"><span class="dot on"></span> käyttämätön</td>
<td>{{fidate .CreatedAt}}</td>
</tr>
{{else}}
<tr><td colspan="3" class="muted">Ei käyttämättömiä kutsuja.</td></tr>
{{end}}
</tbody>
</table>
<p class="muted small">Lähetä linkki kaverille — se avaa liittymislomakkeen koodi valmiiksi
täytettynä. Lista näyttää käyttämättömät kutsut{{if .Data.SpentCount}}; käytettyjä on
{{.Data.SpentCount}}{{end}}.</p>
</div>
</section>
<section class="adminsection">
<header><h2>Jäsenet</h2></header>
<div class="body">
<table>
<thead><tr><th>Nimi</th><th>Sähköposti</th><th>Liittyi</th><th>Toiminnot</th></tr></thead>
<tbody>
{{range .Data.Members}}
<tr{{if .Banned}} class="banned"{{end}}>
<td>{{.Name}}{{if .Banned}} <span class="badge pending">estetty</span>{{end}}</td>
<td>{{.Email}}</td>
<td>{{fidate .CreatedAt}}</td>
<td class="actions">
<form method="post" action="/admin/users/{{.ID}}/ban">
<button type="submit" class="ghost">{{if .Banned}}Poista esto{{else}}Estä{{end}}</button>
</form>
<form method="post" action="/admin/users/{{.ID}}/password">
<input type="password" name="password" placeholder="uusi salasana" required>
<button type="submit" class="ghost">Vaihda salasana</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="4" class="muted">Ei jäseniä. Luo kutsukoodi ja lähetä se jollekulle.</td></tr>
{{end}}
</tbody>
</table>
</div>
</section>
<section class="adminsection">
<header><h2>Kappaleet</h2></header>
<div class="body">
<table>
<thead><tr><th>Kappale</th><th>Lähettäjä</th><th>Arvostelut</th><th>Julkaistu</th><th></th></tr></thead>
<tbody>
{{range .Data.Songs}}
<tr>
<td>{{.Title}} <span class="muted">— {{.Artist}}</span></td>
<td>{{.Submitter}}</td>
<td>{{.Reviews}}</td>
<td class="nowrap">{{fidate .CreatedAt}}</td>
<td class="actions">
<a href="/admin/audio/{{.ID}}">Kuuntele</a>
<form method="post" action="/admin/songs/{{.ID}}/delete"
onsubmit="return confirm('Poistetaanko kappale ja kaikki sen arvostelut?')">
<button type="submit" class="ghost danger">Poista</button>
</form>
</td>
</tr>
{{else}}
<tr><td colspan="5" class="muted">Ei kappaleita.</td></tr>
{{end}}
</tbody>
</table>
</div>
</section>
<script>
// The clipboard API needs a secure context. Over the documented SSH tunnel the origin is
// localhost, which qualifies; reached any other way it is missing, so selecting the text is the
// fallback — the admin presses Ctrl+C instead of being left with a button that does nothing.
function copyInvite(button) {
const link = button.previousElementSibling;
const done = () => {
button.textContent = 'Kopioitu';
setTimeout(() => { button.textContent = 'Kopioi'; }, 1500);
};
if (navigator.clipboard) {
navigator.clipboard.writeText(link.textContent).then(done, () => selectText(link));
} else {
selectText(link);
}
}
function selectText(el) {
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
</script>
{{end}}