Add data-loss guards and first-run empty 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. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1bc695f508
commit
8dffd8ccea
2 changed files with 83 additions and 1 deletions
73
handlers.py
73
handlers.py
|
|
@ -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, '
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue