Harden Stork for public demo and family durability
Read-only demo boards, per-IP rate limits, opaque voter sessions, session leave, upload sniffing, path containment, and retire /v1.
This commit is contained in:
+72
-10
@@ -346,6 +346,22 @@
|
||||
.grid-3 { display: grid; gap: 0.55rem; margin-top: 0.55rem; }
|
||||
@media (min-width: 720px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
|
||||
.hint { color: var(--muted); font-size: 0.9rem; margin: 0.3rem 0 0; }
|
||||
.banner {
|
||||
margin: 0 0 0.85rem; padding: 0.55rem 0.75rem;
|
||||
border: 1px solid var(--rule); background: var(--chip);
|
||||
color: var(--muted); font-size: 0.88rem; line-height: 1.35;
|
||||
}
|
||||
.banner strong { color: var(--ink); }
|
||||
.link-risk { margin: 0.35rem 0 0; color: var(--muted); font-size: 0.82rem; }
|
||||
.board-readonly .add-col,
|
||||
.board-readonly #suggest,
|
||||
.board-readonly .voice-row,
|
||||
.board-readonly .votes,
|
||||
.board-readonly .remove-col,
|
||||
.board-readonly .title-edit { pointer-events: none; opacity: 0.55; }
|
||||
.board-readonly .title-edit { border-color: transparent !important; }
|
||||
.board-readonly .add-col { display: none; }
|
||||
.board-readonly #suggest { display: none; }
|
||||
.foot {
|
||||
margin-top: 1.5rem; padding-top: 0.85rem;
|
||||
border-top: 1px solid var(--rule);
|
||||
@@ -372,7 +388,6 @@
|
||||
|
||||
<div class="topnav">
|
||||
<a href="/logos">Logo concepts</a>
|
||||
<a href="/v1">Classic board</a>
|
||||
</div>
|
||||
|
||||
<header class="hero">
|
||||
@@ -384,7 +399,7 @@
|
||||
</header>
|
||||
|
||||
<section id="landing" class="sheet">
|
||||
<p class="hint" style="margin-top:0">Start a private board, add names, then share the link. No password — the link is the key.</p>
|
||||
<p class="hint" style="margin-top:0">Start a private board, add names, then share the link. No password — the link is the key. Anyone with the link can edit.</p>
|
||||
<label for="board-title">Board title</label>
|
||||
<input id="board-title" value="Name board" autocomplete="off" />
|
||||
<label for="display-new" style="margin-top:0.6rem">Your name</label>
|
||||
@@ -392,6 +407,7 @@
|
||||
<p class="error hidden" id="landing-error" role="alert" aria-live="polite"></p>
|
||||
<div class="row" style="margin-top:0.65rem">
|
||||
<button id="start-board" type="button">Start new board</button>
|
||||
<button type="button" class="ghost hidden" id="continue-board">Continue</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -406,14 +422,20 @@
|
||||
</section>
|
||||
|
||||
<section id="board" class="hidden">
|
||||
<p id="readonly-banner" class="banner hidden" role="status">
|
||||
<strong>Public demo (read-only).</strong> Browse the sample names, then
|
||||
<a href="/">start your own board</a> from home.
|
||||
</p>
|
||||
<div class="who">
|
||||
<span id="who"></span>
|
||||
<span class="who-actions">
|
||||
<button type="button" class="help-chip" data-open-help aria-label="Help" title="Help">?</button>
|
||||
<button type="button" class="ghost" id="share-link" style="padding:0.35rem 0.6rem;font-size:0.85rem">Copy share link</button>
|
||||
<a href="#suggest">Suggest a name</a>
|
||||
<button type="button" class="ghost" id="leave-board" style="padding:0.35rem 0.6rem;font-size:0.85rem">Leave board</button>
|
||||
<a href="#suggest" id="suggest-jump">Suggest a name</a>
|
||||
</span>
|
||||
</div>
|
||||
<p id="link-risk" class="link-risk hidden">Anyone with this link can edit.</p>
|
||||
|
||||
<div class="boards-row">
|
||||
<div class="boards" id="boards"></div>
|
||||
@@ -464,15 +486,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="foot">Paper board · <button type="button" class="help-link" data-open-help>Help</button> · press <kbd>?</kbd> · <a href="/v1">classic layout</a></p>
|
||||
<p class="foot">Paper board · <button type="button" class="help-link" data-open-help>Help</button> · press <kbd>?</kbd></p>
|
||||
</main>
|
||||
<dialog id="help-dialog" class="help-dialog" aria-labelledby="help-title">
|
||||
<div class="help-inner">
|
||||
<h2 id="help-title">How this board works</h2>
|
||||
<h3>Sharing</h3>
|
||||
<ul>
|
||||
<li>The <strong>/b/…</strong> link is the key — no password. Use <strong>Copy share link</strong>.</li>
|
||||
<li>Everyone picks a display name; it shows on votes.</li>
|
||||
<li>The <strong>/b/…</strong> link is the key — no password. Anyone with the link can edit.</li>
|
||||
<li>Use <strong>Copy share link</strong>. Everyone picks a display name for votes.</li>
|
||||
<li>Public demo boards are <strong>read-only</strong> showcases — start your own from home.</li>
|
||||
</ul>
|
||||
<h3>Names & votes</h3>
|
||||
<ul>
|
||||
@@ -505,12 +528,14 @@
|
||||
kind: "c1",
|
||||
boardId: null,
|
||||
boardUrl: null,
|
||||
readonly: false,
|
||||
columns: [],
|
||||
lists: {},
|
||||
openId: null,
|
||||
editId: null,
|
||||
langById: {},
|
||||
recording: null,
|
||||
pendingContinue: null,
|
||||
};
|
||||
let speakingBtn = null;
|
||||
let audioEl = null;
|
||||
@@ -964,9 +989,17 @@
|
||||
document.getElementById("who").textContent = name;
|
||||
state.boardId = board && board.id;
|
||||
state.boardUrl = (board && board.url) || (state.boardId ? `${location.origin}/b/${state.boardId}` : location.href);
|
||||
state.readonly = !!(board && board.readonly);
|
||||
const boardEl = document.getElementById("board");
|
||||
boardEl.classList.toggle("board-readonly", state.readonly);
|
||||
document.getElementById("readonly-banner").classList.toggle("hidden", !state.readonly);
|
||||
document.getElementById("link-risk").classList.toggle("hidden", state.readonly);
|
||||
document.getElementById("suggest-jump").classList.toggle("hidden", state.readonly);
|
||||
const tag = document.getElementById("board-tag");
|
||||
if (board && board.title) {
|
||||
tag.textContent = `${board.title} — vote, edit pronunciation, record your voice when AI misses.`;
|
||||
tag.textContent = state.readonly
|
||||
? `${board.title} — public read-only demo.`
|
||||
: `${board.title} — vote, edit pronunciation, record your voice when AI misses.`;
|
||||
}
|
||||
if (window.speechSynthesis) speechSynthesis.getVoices();
|
||||
await loadColumns();
|
||||
@@ -980,7 +1013,23 @@
|
||||
return m ? decodeURIComponent(m[1]) : "";
|
||||
}
|
||||
|
||||
async function leaveBoard() {
|
||||
try { await api("/api/session", { method: "DELETE" }); } catch (_) {}
|
||||
state.boardId = null;
|
||||
state.boardUrl = null;
|
||||
state.readonly = false;
|
||||
state.pendingContinue = null;
|
||||
history.replaceState({}, "", "/");
|
||||
document.getElementById("board").classList.add("hidden");
|
||||
document.getElementById("gate").classList.add("hidden");
|
||||
document.getElementById("landing").classList.remove("hidden");
|
||||
document.getElementById("continue-board").classList.add("hidden");
|
||||
document.getElementById("board-tag").textContent =
|
||||
"Family name board — vote, edit how a name is said, and record your own voice when TTS misses.";
|
||||
}
|
||||
|
||||
document.getElementById("add-col").addEventListener("click", async () => {
|
||||
if (state.readonly) return;
|
||||
if (state.columns.length >= MAX_COLUMNS) return;
|
||||
try {
|
||||
const col = await api("/api/columns", { method: "POST" });
|
||||
@@ -1009,6 +1058,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("leave-board").addEventListener("click", () => leaveBoard());
|
||||
|
||||
document.getElementById("continue-board").addEventListener("click", async () => {
|
||||
const pending = state.pendingContinue;
|
||||
if (!pending || !pending.board) return;
|
||||
history.replaceState({}, "", `/b/${pending.board.id}`);
|
||||
await showBoard(pending.display_name, pending.board);
|
||||
});
|
||||
|
||||
document.getElementById("start-board").addEventListener("click", async () => {
|
||||
const err = document.getElementById("landing-error");
|
||||
StorkUI.clearError(err);
|
||||
@@ -1134,9 +1192,13 @@
|
||||
try {
|
||||
const session = await api("/api/session");
|
||||
if (session.authenticated && session.board) {
|
||||
history.replaceState({}, "", `/b/${session.board.id}`);
|
||||
await showBoard(session.display_name, session.board);
|
||||
return;
|
||||
state.pendingContinue = {
|
||||
display_name: session.display_name,
|
||||
board: session.board,
|
||||
};
|
||||
const btn = document.getElementById("continue-board");
|
||||
btn.textContent = `Continue to ${session.board.title}`;
|
||||
btn.classList.remove("hidden");
|
||||
}
|
||||
} catch (_) {}
|
||||
document.getElementById("landing").classList.remove("hidden");
|
||||
|
||||
Reference in New Issue
Block a user