From 8dffd8ccea27e0ed5900a58ac1219c8fc308cae2 Mon Sep 17 00:00:00 2001 From: Derick Phan Date: Fri, 24 Apr 2026 09:38:07 -0700 Subject: [PATCH] Add data-loss guards and first-run empty state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- README.md | 11 ++++++++ handlers.py | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de6f038..e44c450 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,17 @@ Your data is stored in `~/.tinyweb/`: This allows your data to persist between upgrades and stay separate from the application. +### Backups + +Back up the whole `~/.tinyweb/` directory periodically. The two files that matter: + +- **`tinyweb_identity`** is your permanent mesh identity. If you lose it, your destination hash changes and every subscriber has to re-subscribe to the new one. Keep it somewhere you trust; the file is `0600` by default. +- **`index.db`** is your full reading history — every page, note, tag, and synced remote page. Losing it loses everything you've curated. + +`models/` and `index.hnsw` are re-derivable (the model will re-download, and the HNSW index rebuilds from the database on next startup with semantic search enabled) so they don't need to be backed up. + +The `/export` page produces a JSON dump of your pages. It's a migration aid — it doesn't preserve your identity file, your custom template, or subscription state. A full restore needs a copy of `~/.tinyweb/`. + ### Docker Data is stored in the `/data` volume inside the container. Use a volume mount to persist data: diff --git a/handlers.py b/handlers.py index 47f38e1..e47520b 100644 --- a/handlers.py +++ b/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 = ( + '
' + '

Your index is empty.

' + '

tinyweb is a personal search engine for pages you save. ' + 'The index stays on your machine; so does every search.

' + '

From here: add a page, ' + 'get the bookmarklet, or ' + 'subscribe to another instance.

' + '
' + ) return _respond( f'
' f'' @@ -341,6 +353,7 @@ def handle_search(query): f'
' f'

{count} pages indexed' f' · + add url

' + 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'
  • {esc(r["title"] or r["url"])}
    ' + f'{esc(r["url"])}
  • ' + for r in rows + ) + hidden_ids = "".join( + f'' for r in rows + ) + n = len(rows) + return _respond( + f"

    confirm delete

    " + f"

    Remove the following {n} page{'' if n == 1 else 's'}?

    " + f"" + f'
    ' + f'{_csrf_field()}' + f'{hidden_ids}' + f'' + f'' + f'' + f"
    " + f' cancel' + ) + + 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"

    Drag this link to your bookmarks bar. Click it on any page to index it instantly.

    " f'

    + save to {esc(name)}

    ' f"

    reset

    " - f'
    ' + f'' f'{_csrf_field()}' f'' f"
    " @@ -960,6 +1015,22 @@ def handle_about(): f'' f'{sharing_html}' f'{hash_html}' + f'

    your data

    ' + f'

    Everything is stored locally under ~/.tinyweb/:

    ' + f'' + f'

    Back up ~/.tinyweb/ periodically. ' + f'Copying the whole directory to another device preserves your identity and index together. ' + f'The export 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.

    ' f'

    what is the slow web?

    ' f'

    The slow web is a movement for intentionality over speed, ' f'human curation over algorithmic feeds, privacy over surveillance, '