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
119 lines
4.3 KiB
HTML
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}}
|