added data-loss guards + first-run state

- Bulk delete now routes through a server-rendered confirmation page
  listing the selected titles; a `confirmed=1` form field is required
  before pages are actually deleted. Mirrors the single-delete flow.
- Reset-template button gains a JS confirm() so stray clicks don't wipe
  the custom template.
- Homepage shows a short, neutral empty-state block when the index has
  zero pages and no query — just names what tinyweb is and links to
  /add, /style, and /subscriptions as equal options.
- /about gains a "your data" section explaining what lives in
  ~/.tinyweb/ (identity file, index.db), what losing each costs, and
  how /export differs from a full backup.
- README gains a "Backups" subsection mirroring the /about copy.
This commit is contained in:
lichenblankie 2026-04-24 09:38:07 -07:00
parent 8205db9bc3
commit 55c6619ba3
2 changed files with 83 additions and 1 deletions

View file

@ -334,6 +334,18 @@ def handle_search(query):
sub_count = ""
if q and remote_rows:
sub_count = f" + {len(remote_rows)} from subscriptions"
welcome_html = ""
if count == 0 and not q:
welcome_html = (
'<section style="margin-top:1.5rem;max-width:40em">'
'<p>Your index is empty.</p>'
'<p>tinyweb is a personal search engine for pages you save. '
'The index stays on your machine; so does every search.</p>'
'<p>From here: <a href="/add">add a page</a>, '
'<a href="/style">get the bookmarklet</a>, or '
'<a href="/subscriptions">subscribe to another instance</a>.</p>'
'</section>'
)
return _respond(
f'<form method="get" action="/">'
f'<input name="q" value="{esc(q)}" placeholder="search your index" size="40">'
@ -341,6 +353,7 @@ def handle_search(query):
f'</form>'
f'<p class="meta">{count} pages indexed'
f' · <a href="/add">+ add url</a></p>'
f'{welcome_html}'
f'{result_html}'
f'{_page_nav(page, total_results, f"/?q={esc(q)}") if q else ""}'
f'{trusted_html}{remote_html}'
@ -547,6 +560,43 @@ def handle_pages(query=None):
)
def _render_bulk_delete_confirm(page_ids):
"""Server-side confirmation page for bulk deletion — mirrors handle_delete_confirm."""
db = get_db()
try:
placeholders = ",".join("?" * len(page_ids))
rows = db.execute(
f"SELECT id, url, title FROM pages WHERE id IN ({placeholders})",
page_ids,
).fetchall()
finally:
return_db(db)
if not rows:
return _redirect("/pages")
items = "".join(
f'<li><b>{esc(r["title"] or r["url"])}</b><br>'
f'<small>{esc(r["url"])}</small></li>'
for r in rows
)
hidden_ids = "".join(
f'<input type="hidden" name="ids" value="{int(r["id"])}">' for r in rows
)
n = len(rows)
return _respond(
f"<h1>confirm delete</h1>"
f"<p>Remove the following {n} page{'' if n == 1 else 's'}?</p>"
f"<ul>{items}</ul>"
f'<form method="post" action="/pages/bulk">'
f'{_csrf_field()}'
f'{hidden_ids}'
f'<input type="hidden" name="action" value="delete">'
f'<input type="hidden" name="confirmed" value="1">'
f'<button type="submit">yes, delete {n} page{"" if n == 1 else "s"}</button>'
f"</form>"
f' <a href="/pages">cancel</a>'
)
def handle_bulk_action(body):
ids = body.get("ids", [])
action = body.get("action", [""])[0]
@ -557,6 +607,10 @@ def handle_bulk_action(body):
page_ids = [int(i) for i in ids]
except ValueError:
return _error(400)
# Require an explicit second-step confirmation for bulk delete — the JS
# confirm() on /pages is a first-line filter only.
if action == "delete" and body.get("confirmed", [""])[0] != "1":
return _render_bulk_delete_confirm(page_ids)
db = get_db()
try:
if action == "delete":
@ -871,7 +925,8 @@ def handle_style_form(msg=""):
f"<p>Drag this link to your bookmarks bar. Click it on any page to index it instantly.</p>"
f'<p><a href="javascript:void(fetch(\'http://localhost:8080/bookmark?url=\'+encodeURIComponent(location.href)+\'&token={_get_bookmark_token()}\').then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>'
f"<h2>reset</h2>"
f'<form method="post" action="/style/reset">'
f'<form method="post" action="/style/reset" '
f'onsubmit="return confirm(\'Reset the template to default? Your custom template will be lost.\')">'
f'{_csrf_field()}'
f'<button type="submit">reset template to default</button>'
f"</form>"
@ -960,6 +1015,22 @@ def handle_about():
f'</ul>'
f'{sharing_html}'
f'{hash_html}'
f'<h2>your data</h2>'
f'<p>Everything is stored locally under <code>~/.tinyweb/</code>:</p>'
f'<ul>'
f'<li><code>tinyweb_identity</code> — your permanent mesh identity. '
f'If you lose this file, your destination hash changes and subscribers '
f'have to re-subscribe to the new one.</li>'
f'<li><code>index.db</code> — your full reading history: every page, '
f'note, tag, and synced remote page.</li>'
f'<li><code>models/</code> — the semantic search model if you enabled it '
f'(redownloadable, safe to delete).</li>'
f'</ul>'
f'<p><b>Back up <code>~/.tinyweb/</code> periodically.</b> '
f'Copying the whole directory to another device preserves your identity and index together. '
f'The <a href="/export">export</a> page gives you a JSON dump of pages only — '
f'it does not preserve your identity or subscription state, so it is a migration aid, '
f'not a substitute for a full backup.</p>'
f'<h2>what is the slow web?</h2>'
f'<p>The slow web is a movement for intentionality over speed, '
f'human curation over algorithmic feeds, privacy over surveillance, '