unified add form, remove page view endpoint, drop mesh nav/section

This commit is contained in:
blankie 2026-06-18 20:34:58 +00:00
parent d0b27b4c61
commit cc71d9c577
6 changed files with 117 additions and 129 deletions

View file

@ -18,7 +18,6 @@ from ._helpers import (
from .search import handle_search
from .pages import (
handle_add_form, handle_add_submit, handle_add_manual_submit,
handle_page_view,
handle_pages, _render_bulk_delete_confirm, handle_bulk_action,
handle_edit_form, handle_edit_submit,
handle_delete_confirm, handle_delete,
@ -66,9 +65,6 @@ def _dispatch_inner(data):
elif path == "/add":
prefill_url = query.get("url", [""])[0].strip()
return handle_add_form(prefill_url=prefill_url)
elif path.startswith("/pages/"):
pid = extract_id("/pages/")
return handle_page_view(pid) if pid is not None else _error(400)
elif path == "/pages":
return handle_pages(query)
elif path.startswith("/edit/"):

View file

@ -29,93 +29,86 @@ def handle_add_form(msg="", action_type="index", prefill_url=""):
)
url_value = f'value="{esc(prefill_url)}" ' if prefill_url else ""
return _respond(
f"<h1>add url</h1>"
f"<p>Add a site to your index</p>"
f"<h1>add site</h1>"
f"<p>Add a site to your index — URL or RNS destination hash</p>"
f'<form method="post" action="/add">'
f'{_csrf_field()}'
f'<input type="hidden" name="input_type" value="url">'
f'<input name="url" placeholder="https://example.com" size="50" {url_value}><br><br>'
f'<input name="url" placeholder="https://example.com or 32-char RNS hash" size="50" {url_value}><br><br>'
f'<input name="note" placeholder="why are you saving this? (optional)" size="50"><br><br>'
f'<input name="tags" placeholder="tags (comma-separated, e.g. solarpunk, mesh)" size="50"><br>'
f'<small>tag: private to exclude from sharing</small><br><br>'
f'<button type="submit">index</button>'
f"</form>"
f"<br><hr><br>"
f"<h2>add mesh site</h2>"
f"<p>Browse a remote TinyWeb instance over Reticulum</p>"
f'<form method="post" action="/add">'
f'{_csrf_field()}'
f'<input type="hidden" name="input_type" value="rns">'
f'<input name="reticulum_dest" placeholder="RNS destination hash (32 hex chars)" size="50" style="font-family:monospace"><br><br>'
f'<input name="name" placeholder="name (optional)" size="50"><br><br>'
f'<button type="submit">add mesh site</button>'
f"</form>"
f"<p>{msg}</p>"
f'<a href="/">back</a>'
)
def handle_add_submit(body):
input_type = body.get("input_type", ["url"])[0]
url = body.get("url", [""])[0].strip()
reticulum_dest = body.get("reticulum_dest", [""])[0].strip().replace("<", "").replace(">", "")
name = body.get("name", [""])[0].strip()
raw = body.get("url", [""])[0].strip().replace("<", "").replace(">", "")
note = body.get("note", [""])[0].strip()
tags = body.get("tags", [""])[0].strip()
if input_type == "rns":
if not reticulum_dest:
return handle_add_form("RNS destination hash is required.")
if len(reticulum_dest) != 32 or not all(c in "0123456789abcdefABCDEF" for c in reticulum_dest):
return handle_add_form("Invalid RNS destination hash. Must be 32 hex characters.")
if not raw:
return handle_add_form("URL or RNS hash is required.")
is_rns = (
len(raw) == 32
and all(c in "0123456789abcdefABCDEF" for c in raw)
)
if raw.startswith("rns:") or raw.startswith("RNS:"):
raw = raw[4:]
is_rns = (
len(raw) == 32
and all(c in "0123456789abcdefABCDEF" for c in raw)
)
if is_rns:
from .rns import handle_rns_add_hash
handle_rns_add_hash(reticulum_dest, name)
errs = handle_rns_add_hash(raw)
if errs:
return handle_add_form(f"Hash saved but indexing failed: {'; '.join(errs)}")
return _redirect("/")
if input_type == "url":
if not url:
return handle_add_form("URL is required.")
url = clean_url(url)
if not url.startswith(("http://", "https://")):
return handle_add_form("URL must start with http:// or https://")
url = clean_url(raw)
if not url.startswith(("http://", "https://")):
return handle_add_form("Enter a URL (http:// or https://) or a 32-char RNS destination hash.")
try:
title = index_url(url, note)
if tags:
db = get_db()
try:
row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()
if row:
_set_page_tags(row["id"], tags, db)
db.commit()
finally:
return_db(db)
return handle_add_form(f'Indexed: {esc(url)}')
except ValueError as e:
return handle_add_form(f"Error: {esc(str(e))}")
except Exception as e:
error_msg = str(e).lower()
if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")):
return _respond(
f"<h1>add url (manual entry)</h1>"
f"<p><strong>{esc(url)}</strong> blocks automated access. "
f"You can still save it manually:</p>"
f'<form method="post" action="/add/manual">'
f'{_csrf_field()}'
f'<input type="hidden" name="url" value="{esc(url)}">'
f'<input type="hidden" name="note" value="{esc(note)}">'
f'<input type="hidden" name="tags" value="{esc(tags)}">'
f'<label>Title:</label><br>'
f'<input name="manual_title" size="50" placeholder="page title" required><br><br>'
f'<label>Description:</label><br>'
f'<textarea name="manual_description" rows="4" cols="50" placeholder="what is this site about? (optional)"></textarea><br><br>'
f'<button type="submit">save manually</button>'
f"</form>"
f'<a href="/">back</a>'
)
return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}")
else:
return handle_add_form("Invalid input type.")
try:
title = index_url(url, note)
if tags:
db = get_db()
try:
row = db.execute("SELECT id FROM pages WHERE url = ?", (url,)).fetchone()
if row:
_set_page_tags(row["id"], tags, db)
db.commit()
finally:
return_db(db)
return handle_add_form(f'Indexed: {esc(url)}')
except ValueError as e:
return handle_add_form(f"Error: {esc(str(e))}")
except Exception as e:
error_msg = str(e).lower()
if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")):
return _respond(
f"<h1>add url (manual entry)</h1>"
f"<p><strong>{esc(url)}</strong> blocks automated access. "
f"You can still save it manually:</p>"
f'<form method="post" action="/add/manual">'
f'{_csrf_field()}'
f'<input type="hidden" name="url" value="{esc(url)}">'
f'<input type="hidden" name="note" value="{esc(note)}">'
f'<input type="hidden" name="tags" value="{esc(tags)}">'
f'<label>Title:</label><br>'
f'<input name="manual_title" size="50" placeholder="page title" required><br><br>'
f'<label>Description:</label><br>'
f'<textarea name="manual_description" rows="4" cols="50" placeholder="what is this site about? (optional)"></textarea><br><br>'
f'<button type="submit">save manually</button>'
f"</form>"
f'<a href="/">back</a>'
)
return handle_add_form(f"Error: could not fetch or index that URL. {esc(str(e)[:100])}")
def handle_add_manual_submit(body):
@ -162,32 +155,6 @@ def handle_add_manual_submit(body):
return_db(db)
def handle_page_view(page_id):
db = get_db()
try:
row = db.execute("SELECT id, url, title, body, note FROM pages WHERE id = ?", (page_id,)).fetchone()
if not row:
return _error(404)
tags = _get_page_tags(row["id"], db)
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags) if tags else ""
note_html = f'<p><em>{esc(row["note"])}</em></p>' if row["note"] else ""
title = esc(row["title"] or "(untitled)")
body_html = row["body"] or "(no content)"
url_link = f'<p><small>source: <a href="{esc(row["url"])}" rel="noreferrer noopener">{esc(row["url"])}</a></small></p>'
return _respond(
f"<h1>{title}</h1>"
f"{tag_links}"
f"{note_html}"
f"<hr>"
f"{body_html}"
f"<hr>"
f"{url_link}"
f'<a href="/pages">back to browse</a>'
)
finally:
return_db(db)
def handle_pages(query=None):
msg = query.get("msg", [""])[0] if query else ""
msg_html = f'<p class="success">{esc(msg)}</p>' if msg else ""
@ -208,10 +175,17 @@ def handle_pages(query=None):
if tags:
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
tags_html = f' {tag_links}'
url = r["url"]
if url.startswith("rns:"):
display_url = url
link_url = f"/rns/{esc(url[4:])}/"
else:
display_url = url
link_url = url
items += (
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
f'<a href="/pages/{r["id"]}">{esc(r["title"])}</a></label>{note_html}{tags_html} '
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> '
f'{esc(r["title"])}</label>{note_html}{tags_html} '
f'<small>(<a href="{esc(link_url)}" rel="noreferrer noopener">{esc(display_url)}</a>)</small> '
f'<a href="/edit/{r["id"]}?p={page}">edit</a> '
f'<a href="/delete/{r["id"]}">remove</a></li>'
)

View file

@ -1,4 +1,5 @@
import json
import traceback
from tinyweb.db import get_db, return_db
from tinyweb.rns_client import fetch_remote_page
from tinyweb.templates import esc
@ -14,15 +15,49 @@ def _get_mesh_sites():
def handle_rns_add_hash(dest_hash, name=""):
db = get_db()
errors = []
try:
db.execute(
"INSERT OR REPLACE INTO mesh_sites (hash, name) VALUES (?, ?)",
(dest_hash, name or ""),
)
try:
resp = fetch_remote_page(dest_hash, "/")
if resp.get("status") == 200:
body = resp.get("body", "")
title = name or dest_hash[:16]
import re
m = re.search(r"<title[^>]*>(.*?)</title>", body, re.IGNORECASE | re.DOTALL)
if m:
title = m.group(1).strip()
desc = ""
m = re.search(r'<meta\s+name="description"\s+content="([^"]*)"', body, re.IGNORECASE)
if m:
desc = m.group(1).strip()
if not desc:
text = re.sub(r"<[^>]+>", " ", body)
text = re.sub(r"\s+", " ", text).strip()
desc = text[:200].strip()
url = f"rns:{dest_hash}"
import datetime
now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
db.execute(
"INSERT OR REPLACE INTO pages (url, title, body, last_modified, summary) VALUES (?, ?, ?, ?, ?)",
(url, title, body, now, desc),
)
else:
errors.append(f"Remote returned status {resp.get('status')}")
except Exception as e:
errors.append(str(e))
traceback.print_exc()
db.commit()
finally:
return_db(db)
return errors if errors else None
def handle_rns_delete_hash(body):
dest_hash = body.get("hash", [""])[0].strip() if isinstance(body, dict) else body

View file

@ -1,6 +1,6 @@
from tinyweb.db import get_db, return_db, get_setting, get_site_name, clean_url
from tinyweb.templates import esc
from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, _csrf_field, PER_PAGE
from ._helpers import _sanitize_fts_query, _paginate, _get_page_tags, _respond, _page_nav, PER_PAGE
def handle_search(query):
@ -83,10 +83,17 @@ def handle_search(query):
tag_links = " ".join(f'<a href="/tags/{esc(t)}" class="tag">[{esc(t)}]</a>' for t in tags)
tags_html = f'<div class="tags">{tag_links}</div>'
snip_html = f'<br>{esc(r["summary"])}' if r["summary"] else ""
url = r["url"]
if url.startswith("rns:"):
display_url = url
link_url = f"/rns/{esc(url[4:])}/"
else:
display_url = url
link_url = url
result_html += (
f'<div class="result">'
f'<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["title"])}</a><br>'
f'<small>{esc(r["url"])}</small>'
f'<a href="{esc(link_url)}" rel="noreferrer noopener">{esc(r["title"])}</a><br>'
f'<small>{esc(display_url)}</small>'
f'{snip_html}'
f'{note_html}{tags_html}'
f'</div>'
@ -163,30 +170,6 @@ def handle_search(query):
if q and remote_rows:
sub_count = f" + {len(remote_rows)} from subscriptions"
mesh_html = ""
if not q:
from .rns import _get_mesh_sites
sites = _get_mesh_sites()
if sites:
rows_html = ""
for s in sites:
rows_html += (
f'<tr>'
f'<td><a href="/rns/{esc(s["hash"])}/">{esc(s["name"] or s["hash"][:16])}</a></td>'
f'<td style="font-family:monospace;font-size:0.85rem">{esc(s["hash"])}</td>'
f'<td><form action="/rns/delete" method="POST" style="display:inline">'
f'{_csrf_field()}'
f'<input type="hidden" name="hash" value="{esc(s["hash"])}">'
f'<button type="submit">delete</button>'
f'</form></td>'
f'</tr>'
)
mesh_html = (
f"<br>"
f"<h2>mesh sites</h2>"
f'<table>{rows_html}</table>'
)
welcome_html = ""
if count == 0 and not q:
welcome_html = (
@ -209,5 +192,5 @@ def handle_search(query):
f'{welcome_html}'
f'{result_html}'
f'{_page_nav(page, total_results, f"/?q={esc(q)}") if q else ""}'
f'{trusted_html}{remote_html}{mesh_html}'
f'{trusted_html}{remote_html}'
)

View file

@ -45,7 +45,7 @@ def handle_tag_browse(tag_name, query=None):
tags = _get_page_tags(r["id"], db)
tag_links = " ".join(f'<a href="/tags/{esc(t)}">[{esc(t)}]</a>' for t in tags)
items += (
f'<li><a href="/pages/{r["id"]}">{esc(r["title"])}</a>{note_html} {tag_links} '
f'<li>{esc(r["title"])}{note_html} {tag_links} '
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small></li>'
)
finally:

View file

@ -13,7 +13,7 @@ def _nav_html():
return (
f'<p><b><a href="/">{name}</a></b>'
' | <a href="/">search</a> | <a href="/pages">browse</a>'
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a> | <a href="/rns">mesh</a>'
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
f'{forum_link}'
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
"<hr>\n"