From 8e68d02413fea7c96e4da7d53c432a89538901d8 Mon Sep 17 00:00:00 2001 From: blankie Date: Tue, 9 Jun 2026 05:35:54 +0000 Subject: [PATCH 01/14] search: match by tag; add: handle ssl errors with manual entry --- handlers/pages.py | 2 +- handlers/search.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/handlers/pages.py b/handlers/pages.py index 3e213e0..cd3efba 100644 --- a/handlers/pages.py +++ b/handlers/pages.py @@ -83,7 +83,7 @@ def handle_add_submit(body): except Exception as e: error_msg = str(e).lower() - if "block" in error_msg or "cloudflare" in error_msg or "403" in error_msg: + if any(x in error_msg for x in ("block", "cloudflare", "403", "ssl", "handshake", "max retries", "timeout", "connection")): return _respond( f"

add url (manual entry)

" f"

{esc(url)} blocks automated access. " diff --git a/handlers/search.py b/handlers/search.py index 967b59c..d9ee7c1 100644 --- a/handlers/search.py +++ b/handlers/search.py @@ -41,6 +41,23 @@ def handle_search(query): else: fused_ids = bm25_ids + # Also match by tag + search_terms = [w.lower() for w in q.split() if w] + if search_terms: + placeholders = ",".join("?" * len(search_terms)) + tag_rows = db.execute( + f"SELECT DISTINCT pt.page_id FROM page_tags pt " + f"JOIN tags t ON t.id = pt.tag_id " + f"WHERE LOWER(t.name) IN ({placeholders})", + search_terms, + ).fetchall() + tag_ids = {r["page_id"] for r in tag_rows} + seen = set(fused_ids) + for pid in tag_ids: + if pid not in seen: + fused_ids.append(pid) + seen.add(pid) + total_results = len(fused_ids) page_ids = fused_ids[offset:offset + PER_PAGE] From e0a5d44d94caad13bbb92f892ed305ed893fd4cd Mon Sep 17 00:00:00 2001 From: blankie Date: Tue, 9 Jun 2026 05:58:55 +0000 Subject: [PATCH 02/14] readme: remove duplicate forum section --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 2405ee0..991c981 100644 --- a/README.md +++ b/README.md @@ -105,14 +105,6 @@ tmux new-session -d -s tinyweb 'python app.py' nohup python app.py & ``` -### Forum plugin (optional) - -```bash -pip install tinyweb-forum -``` - -Enable the forum on the `/style` page. See the [tinyweb-forum README](https://codeberg.org/tinyweb/tinyweb-forum) for details. - ### Docker A `docker-compose.yml` is included for containerized setups. Build and run: From 25e24efbfa03a0c46b1c23f9a001b7c415e8c160 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 07:32:58 +0000 Subject: [PATCH 03/14] simplify: move subscribe form to GET /subscriptions/add --- handlers/__init__.py | 8 +++----- handlers/subscriptions.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index 95d1753..f05dabb 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -59,12 +59,8 @@ def _dispatch_inner(data): if path == "/": return handle_search(query) elif path == "/add": - action_type = query.get("type", ["index"])[0] prefill_url = query.get("url", [""])[0].strip() - return handle_add_form( - action_type=action_type if action_type == "subscribe" else "index", - prefill_url=prefill_url, - ) + return handle_add_form(prefill_url=prefill_url) elif path == "/pages": return handle_pages(query) elif path.startswith("/edit/"): @@ -96,6 +92,8 @@ def _dispatch_inner(data): return handle_api_sites(query) elif path == "/subscriptions": return handle_subscriptions() + elif path == "/subscriptions/add": + return handle_add_form(action_type="subscribe") elif path.startswith("/subscriptions/browse/"): sid = extract_id("/subscriptions/browse/") return handle_subscription_browse(sid) if sid is not None else _error(400) diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index b54a253..e0ecf68 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -198,7 +198,7 @@ def handle_subscriptions(msg=""): f' ' f'' f'' - f'

or subscribe to an instance

' + f'

or subscribe to an instance

' f'

{msg}

' f'
{listing}' f'
back' From 0da46d29fc50f02eda0b863106a81b5ff3e5bf65 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 07:33:38 +0000 Subject: [PATCH 04/14] Revert "simplify: move subscribe form to GET /subscriptions/add" This reverts commit 25e24efbfa03a0c46b1c23f9a001b7c415e8c160. --- handlers/__init__.py | 8 +++++--- handlers/subscriptions.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index f05dabb..95d1753 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -59,8 +59,12 @@ def _dispatch_inner(data): if path == "/": return handle_search(query) elif path == "/add": + action_type = query.get("type", ["index"])[0] prefill_url = query.get("url", [""])[0].strip() - return handle_add_form(prefill_url=prefill_url) + return handle_add_form( + action_type=action_type if action_type == "subscribe" else "index", + prefill_url=prefill_url, + ) elif path == "/pages": return handle_pages(query) elif path.startswith("/edit/"): @@ -92,8 +96,6 @@ def _dispatch_inner(data): return handle_api_sites(query) elif path == "/subscriptions": return handle_subscriptions() - elif path == "/subscriptions/add": - return handle_add_form(action_type="subscribe") elif path.startswith("/subscriptions/browse/"): sid = extract_id("/subscriptions/browse/") return handle_subscription_browse(sid) if sid is not None else _error(400) diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index e0ecf68..b54a253 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -198,7 +198,7 @@ def handle_subscriptions(msg=""): f' ' f'' f'' - f'

or subscribe to an instance

' + f'

or subscribe to an instance

' f'

{msg}

' f'
{listing}' f'
back' From 4a45700067d2112e3f616f5e0d91227aa20ab8d2 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 07:36:37 +0000 Subject: [PATCH 05/14] simplify: move subscribe form to GET /subscriptions/add --- handlers/__init__.py | 8 +++----- handlers/subscriptions.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index 95d1753..f05dabb 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -59,12 +59,8 @@ def _dispatch_inner(data): if path == "/": return handle_search(query) elif path == "/add": - action_type = query.get("type", ["index"])[0] prefill_url = query.get("url", [""])[0].strip() - return handle_add_form( - action_type=action_type if action_type == "subscribe" else "index", - prefill_url=prefill_url, - ) + return handle_add_form(prefill_url=prefill_url) elif path == "/pages": return handle_pages(query) elif path.startswith("/edit/"): @@ -96,6 +92,8 @@ def _dispatch_inner(data): return handle_api_sites(query) elif path == "/subscriptions": return handle_subscriptions() + elif path == "/subscriptions/add": + return handle_add_form(action_type="subscribe") elif path.startswith("/subscriptions/browse/"): sid = extract_id("/subscriptions/browse/") return handle_subscription_browse(sid) if sid is not None else _error(400) diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index b54a253..e0ecf68 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -198,7 +198,7 @@ def handle_subscriptions(msg=""): f' ' f'' f'' - f'

or subscribe to an instance

' + f'

or subscribe to an instance

' f'

{msg}

' f'
{listing}' f'
back' From 27147bd3f966e8849a03a9d8bdc29cccc0d7c090 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 07:43:26 +0000 Subject: [PATCH 06/14] bookmarklet: use dynamic host and scheme from request headers --- gateway.py | 1 + handlers/__init__.py | 9 +++++---- handlers/customize.py | 12 +++++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/gateway.py b/gateway.py index 7d3fd53..02a2d95 100644 --- a/gateway.py +++ b/gateway.py @@ -125,6 +125,7 @@ class GatewayHandler(BaseHTTPRequestHandler): "body": body, "cookies": cookies, "gateway_host": self.headers.get("Host", f"localhost:{GATEWAY_PORT}"), + "scheme": self.headers.get("X-Forwarded-Proto", "http"), } try: diff --git a/handlers/__init__.py b/handlers/__init__.py index f05dabb..ba91d9a 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -48,6 +48,7 @@ def _dispatch_inner(data): query = data.get("query", {}) body = data.get("body", {}) gateway_host = data.get("gateway_host", "") + scheme = data.get("scheme", "http") def extract_id(prefix): try: @@ -72,7 +73,7 @@ def _dispatch_inner(data): elif path == "/bookmark": return handle_bookmark(query) elif path == "/style": - return handle_style_form() + return handle_style_form(gateway_host=gateway_host, scheme=scheme) elif path == "/share/preview": return handle_share_preview() elif path == "/about": @@ -121,14 +122,14 @@ def _dispatch_inner(data): pid = extract_id("/delete/") return handle_delete(pid) if pid is not None else _error(400) elif path == "/style": - return handle_style_submit(body) + return handle_style_submit(body, gateway_host=gateway_host, scheme=scheme) elif path == "/style/reset": set_setting("custom_template", "") - return handle_style_form("Template reset to default.") + return handle_style_form("Template reset to default.", gateway_host=gateway_host, scheme=scheme) elif path == "/style/vacuum": from db import vacuum_db vacuum_db() - return handle_style_form("Database vacuumed.") + return handle_style_form("Database vacuumed.", gateway_host=gateway_host, scheme=scheme) elif path == "/import": return handle_import_submit(body) elif path == "/reindex": diff --git a/handlers/customize.py b/handlers/customize.py index 1f2568b..6e158ed 100644 --- a/handlers/customize.py +++ b/handlers/customize.py @@ -5,7 +5,7 @@ from ._helpers import _respond, _csrf_field, _get_bookmark_token from .subscriptions import _count_shared_pages -def handle_style_form(msg=""): +def handle_style_form(msg="", gateway_host="", scheme="http"): template = get_setting("custom_template") or DEFAULT_TEMPLATE name = get_site_name() sharing = get_setting("sharing_enabled", "0") @@ -135,7 +135,7 @@ def handle_style_form(msg=""): f"" f"

bookmarklet

" f"

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

" - f'

+ save to {esc(name)}

' + f'

r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}

' f"

reset

" f'
' @@ -153,7 +153,7 @@ def handle_style_form(msg=""): ) -def handle_style_submit(body): +def handle_style_submit(body, gateway_host="", scheme="http"): template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") name = body.get("site_name", ["tinyweb"])[0].strip() sharing = "1" if body.get("sharing_enabled") else "0" @@ -192,7 +192,8 @@ def handle_style_submit(body): from handlers import forum_plugin if forum_enabled == "1" and forum_plugin is None: return handle_style_form( - "Forum plugin not installed. Run: pip install tinyweb-forum" + "Forum plugin not installed. Run: pip install tinyweb-forum", + gateway_host=gateway_host, scheme=scheme, ) if forum_enabled == "1": forum_plugin.enable() @@ -208,7 +209,8 @@ def handle_style_submit(body): pass set_setting("forum_enabled", forum_enabled) templates_mod.FORUM_ENABLED = (forum_enabled == "1") - return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.") + return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.", + gateway_host=gateway_host, scheme=scheme) def handle_about(): From 1461c62c9151b1c4e3f8ee105ae7487a07b52c01 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 07:48:34 +0000 Subject: [PATCH 07/14] docker: fix data persistence with TINYWEB_DATA_DIR env var --- Dockerfile | 4 +--- app.py | 2 +- db.py | 2 +- entrypoint.sh | 1 + 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 713df67..3f73263 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -RUN mkdir -p /data \ - && ln -sf /data/index.db index.db \ - && ln -sf /data/tinyweb_identity tinyweb_identity +RUN mkdir -p /data ENV PYTHONUNBUFFERED=1 diff --git a/app.py b/app.py index 6e70196..035eca0 100644 --- a/app.py +++ b/app.py @@ -16,7 +16,7 @@ from gateway import GatewayState, GatewayHandler IDENTITY_FILE = "tinyweb_identity" DEFAULT_TRANSPORT_HOST = "rnode.bre.land" DEFAULT_TRANSPORT_PORT = 4242 -DATA_DIR = os.path.expanduser("~/.tinyweb") +DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb") def get_transport_config(): diff --git a/db.py b/db.py index ec13254..97378d9 100644 --- a/db.py +++ b/db.py @@ -6,7 +6,7 @@ import os from urllib.parse import urlparse, urljoin, parse_qs, urlencode, urlunparse, quote from bs4 import BeautifulSoup -DATA_DIR = os.path.expanduser("~/.tinyweb") +DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb") DATABASE = os.path.join(DATA_DIR, "index.db") BLOCKED_NETWORKS = [ diff --git a/entrypoint.sh b/entrypoint.sh index 7385146..b741c4c 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -29,6 +29,7 @@ if [ ! -f "$CONFIG_FILE" ]; then EOF fi +export TINYWEB_DATA_DIR="/data" export RNS_CONFIG_DIR="$CONFIG_DIR" # Bind to 0.0.0.0 inside the container; isolation is handled by Docker's port mapping. exec python app.py --bind 0.0.0.0 "$@" From 501d9e1838f5c2ff9d7902ac7483f4f9c12d94ad Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 08:00:57 +0000 Subject: [PATCH 08/14] template: add {{nav}} placeholder, separate from {{content}} --- templates.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/templates.py b/templates.py index 777ed63..e0edb19 100644 --- a/templates.py +++ b/templates.py @@ -34,21 +34,25 @@ ul + .forum-form { margin-top: 1rem; } .section ul { margin: 0.3rem 0; } """ -DEFAULT_TEMPLATE = "\n\n\n\n" + FORUM_CSS + "\n\n{{content}}\n\n" - - -def _default_template(): +def _nav_html(): name = esc(get_setting("site_name", "tinyweb")) forum_link = ' | forum' if FORUM_ENABLED else "" return ( - '\n\n\n\n' - f'{FORUM_CSS}\n\n' f'

{name}' ' | search | browse' ' | tags | subscriptions' f'{forum_link}' ' | customize | about

\n' - "
\n{{content}}\n\n" + "
\n" + ) + +DEFAULT_TEMPLATE = "\n\n\n\n" + FORUM_CSS + "\n\n{{nav}}{{content}}\n\n" + + +def _default_template(): + return ( + '\n\n\n\n' + + FORUM_CSS + '\n\n{{nav}}{{content}}\n\n' ) @@ -62,6 +66,7 @@ def wrap_page(body_html, use_default=False): forum_link = ' forum' if FORUM_ENABLED else "" template = template.replace("{{forum_link}}", forum_link) template = template.replace("{{site_name}}", esc(get_setting("site_name", "tinyweb"))) + template = template.replace("{{nav}}", _nav_html()) # Inject forum layout CSS into for any template head_end = "" if head_end in template and FORUM_CSS not in template: From d97ba6d4895691e6f75bb7945b5bca2c420779fe Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 08:05:20 +0000 Subject: [PATCH 09/14] template: remove forum css from DEFAULT_TEMPLATE (injected at render time) --- templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates.py b/templates.py index e0edb19..89dadd7 100644 --- a/templates.py +++ b/templates.py @@ -46,13 +46,13 @@ def _nav_html(): "
\n" ) -DEFAULT_TEMPLATE = "\n\n\n\n" + FORUM_CSS + "\n\n{{nav}}{{content}}\n\n" +DEFAULT_TEMPLATE = "\n\n\n\n\n\n{{nav}}{{content}}\n\n" def _default_template(): return ( '\n\n\n\n' - + FORUM_CSS + '\n\n{{nav}}{{content}}\n\n' + '\n\n{{nav}}{{content}}\n\n' ) From 117fc1312c132763ca7b193049b3f2b225b4e706 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 08:12:10 +0000 Subject: [PATCH 10/14] customize: move save button above html textarea, add unsaved changes indicator --- handlers/customize.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/handlers/customize.py b/handlers/customize.py index 6e158ed..098f56b 100644 --- a/handlers/customize.py +++ b/handlers/customize.py @@ -127,12 +127,22 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): f'manage semantic index

' f"" f"{forum_section}" + f'' + f'' + f'' + f"" f"

custom html

" f"

Edit the full page template. Use {esc('{{content}}')} " f"where page content should appear.

" - f'

' - f'' - f"" + f'
' + f'Saved with the form above.

' f"

bookmarklet

" f"

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

" f'

r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}

' From 076390cbee6ea09305b8abafd3b16bffcf4562c3 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 08:14:43 +0000 Subject: [PATCH 11/14] customize: split into separate settings and template forms --- handlers/__init__.py | 4 +++- handlers/customize.py | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index ba91d9a..0b4365e 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -32,7 +32,7 @@ from .subscriptions import ( handle_subscription_delete, handle_subscription_syncall, _sync_threads, ) -from .customize import handle_style_form, handle_style_submit, handle_about +from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_about from .tags import handle_tags, handle_tag_browse from .data import ( handle_export, handle_import_form, handle_import_submit, @@ -123,6 +123,8 @@ def _dispatch_inner(data): return handle_delete(pid) if pid is not None else _error(400) elif path == "/style": return handle_style_submit(body, gateway_host=gateway_host, scheme=scheme) + elif path == "/style/template": + return handle_style_template_submit(body, gateway_host=gateway_host, scheme=scheme) elif path == "/style/reset": set_setting("custom_template", "") return handle_style_form("Template reset to default.", gateway_host=gateway_host, scheme=scheme) diff --git a/handlers/customize.py b/handlers/customize.py index 098f56b..02ad2d2 100644 --- a/handlers/customize.py +++ b/handlers/customize.py @@ -127,22 +127,16 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): f'manage semantic index

' f"" f"{forum_section}" - f'' - f'' - f'' + f'' f"" f"

custom html

" f"

Edit the full page template. Use {esc('{{content}}')} " f"where page content should appear.

" + f'
' + f'{_csrf_field()}' f'
' - f'Saved with the form above.

' + f'' + f"
" f"

bookmarklet

" f"

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

" f'

r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}

' @@ -223,6 +217,12 @@ def handle_style_submit(body, gateway_host="", scheme="http"): gateway_host=gateway_host, scheme=scheme) +def handle_style_template_submit(body, gateway_host="", scheme="http"): + template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") + set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") + return handle_style_form("Template saved.", gateway_host=gateway_host, scheme=scheme) + + def handle_about(): name = get_site_name() dest_hash = get_setting("dest_hash") From a72de2bb102f5afd08da9d78ae96a004dd3736b6 Mon Sep 17 00:00:00 2001 From: blankie Date: Sun, 14 Jun 2026 08:46:21 +0000 Subject: [PATCH 12/14] auto-save: settings save on change via /style/field, noscript fallback button --- handlers/__init__.py | 4 +++- handlers/customize.py | 50 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/handlers/__init__.py b/handlers/__init__.py index 0b4365e..e7eda3e 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -32,7 +32,7 @@ from .subscriptions import ( handle_subscription_delete, handle_subscription_syncall, _sync_threads, ) -from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_about +from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_field_save, handle_about from .tags import handle_tags, handle_tag_browse from .data import ( handle_export, handle_import_form, handle_import_submit, @@ -125,6 +125,8 @@ def _dispatch_inner(data): return handle_style_submit(body, gateway_host=gateway_host, scheme=scheme) elif path == "/style/template": return handle_style_template_submit(body, gateway_host=gateway_host, scheme=scheme) + elif path == "/style/field": + return handle_field_save(body) elif path == "/style/reset": set_setting("custom_template", "") return handle_style_form("Template reset to default.", gateway_host=gateway_host, scheme=scheme) diff --git a/handlers/customize.py b/handlers/customize.py index 02ad2d2..6808046 100644 --- a/handlers/customize.py +++ b/handlers/customize.py @@ -1,7 +1,7 @@ from db import get_db, return_db, get_setting, set_setting, get_site_name import templates as templates_mod from templates import esc, DEFAULT_TEMPLATE -from ._helpers import _respond, _csrf_field, _get_bookmark_token +from ._helpers import _respond, _json_response, _csrf_field, _get_bookmark_token from .subscriptions import _count_shared_pages @@ -54,7 +54,7 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): return _respond( f"

customize

" f"

name your search engine

" - f'
' + f'' f'{_csrf_field()}' f'

' f"

sharing

" @@ -127,8 +127,26 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): f'manage semantic index

' f"" f"{forum_section}" - f'' + f'

' + f'' f"
" + '' f"

custom html

" f"

Edit the full page template. Use {esc('{{content}}')} " f"where page content should appear.

" @@ -223,6 +241,32 @@ def handle_style_template_submit(body, gateway_host="", scheme="http"): return handle_style_form("Template saved.", gateway_host=gateway_host, scheme=scheme) +def handle_field_save(body): + key = body.get("key", [""])[0].strip() + value = body.get("value", [""])[0].strip() + if not key: + return _json_response({"status": "error", "message": "No key provided."}, 400) + if key == "forum_enabled": + from handlers import forum_plugin + if value == "1" and forum_plugin is None: + return _json_response({"status": "error", "message": "Forum plugin not installed."}, 400) + if value == "1": + forum_plugin.enable() + try: + forum_plugin.fdb.set_setting("forum_enabled", "1") + except Exception: + pass + else: + forum_plugin.disable() + try: + forum_plugin.fdb.set_setting("forum_enabled", "0") + except Exception: + pass + templates_mod.FORUM_ENABLED = (value == "1") + set_setting(key, value) + return _json_response({"status": "ok", "message": ""}) + + def handle_about(): name = get_site_name() dest_hash = get_setting("dest_hash") From 7a81b519bb1fbe037435d787799f127a1ce7d975 Mon Sep 17 00:00:00 2001 From: blankie Date: Tue, 16 Jun 2026 05:11:37 +0000 Subject: [PATCH 13/14] remove demo pages, rename themes, add Content-Length header --- gateway.py | 9 +- handlers/__init__.py | 10 +- handlers/_helpers.py | 4 +- handlers/customize.py | 339 +-- handlers/pages.py | 15 +- handlers/subscriptions.py | 17 +- templates.py | 35 +- themes/{tinyweb-site.html => default.html} | 0 themes/kodama.html | 2169 +++++++++++++------- themes/kodama2.html | 1424 ------------- 10 files changed, 1652 insertions(+), 2370 deletions(-) rename themes/{tinyweb-site.html => default.html} (100%) delete mode 100644 themes/kodama2.html diff --git a/gateway.py b/gateway.py index 02a2d95..fa4b076 100644 --- a/gateway.py +++ b/gateway.py @@ -165,12 +165,15 @@ class GatewayHandler(BaseHTTPRequestHandler): "style-src 'self' 'unsafe-inline'; " "script-src 'self' 'unsafe-inline'; " "img-src 'self' data:") + resp_body = resp.get("body", "") + encoded = resp_body.encode() if isinstance(resp_body, str) else resp_body + if encoded: + self.send_header("Content-Length", str(len(encoded))) for k, v in resp.get("headers", {}).items(): self.send_header(k, v) self.end_headers() - resp_body = resp.get("body", "") - if resp_body: - self.wfile.write(resp_body.encode() if isinstance(resp_body, str) else resp_body) + if encoded: + self.wfile.write(encoded) except ConnectionError as e: GatewayState.link = None diff --git a/handlers/__init__.py b/handlers/__init__.py index e7eda3e..228508e 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -32,7 +32,7 @@ from .subscriptions import ( handle_subscription_delete, handle_subscription_syncall, _sync_threads, ) -from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_field_save, handle_about +from .customize import handle_style_form, handle_style_submit, handle_style_template_submit, handle_field_save, handle_about, _set_flash from .tags import handle_tags, handle_tag_browse from .data import ( handle_export, handle_import_form, handle_import_submit, @@ -66,7 +66,7 @@ def _dispatch_inner(data): return handle_pages(query) elif path.startswith("/edit/"): pid = extract_id("/edit/") - return handle_edit_form(pid) if pid is not None else _error(400) + return handle_edit_form(pid, page=query.get("p", [""])[0]) if pid is not None else _error(400) elif path.startswith("/delete/"): pid = extract_id("/delete/") return handle_delete_confirm(pid) if pid is not None else _error(400) @@ -129,11 +129,13 @@ def _dispatch_inner(data): return handle_field_save(body) elif path == "/style/reset": set_setting("custom_template", "") - return handle_style_form("Template reset to default.", gateway_host=gateway_host, scheme=scheme) + _set_flash("Template reset to default.") + return _redirect("/style") elif path == "/style/vacuum": from db import vacuum_db vacuum_db() - return handle_style_form("Database vacuumed.", gateway_host=gateway_host, scheme=scheme) + _set_flash("Database vacuumed.") + return _redirect("/style") elif path == "/import": return handle_import_submit(body) elif path == "/reindex": diff --git a/handlers/_helpers.py b/handlers/_helpers.py index 3fecb71..2611ce7 100644 --- a/handlers/_helpers.py +++ b/handlers/_helpers.py @@ -65,11 +65,11 @@ def _get_bookmark_token(): return token -def _respond(body_html, status=200, use_default=False): +def _respond(body_html, status=200, use_default=False, head_html=""): return { "status": status, "content_type": "text/html; charset=utf-8", - "body": wrap_page(body_html, use_default=use_default), + "body": wrap_page(body_html, use_default=use_default, head_html=head_html), "headers": {}, } diff --git a/handlers/customize.py b/handlers/customize.py index 6808046..6b3786d 100644 --- a/handlers/customize.py +++ b/handlers/customize.py @@ -1,9 +1,19 @@ from db import get_db, return_db, get_setting, set_setting, get_site_name import templates as templates_mod from templates import esc, DEFAULT_TEMPLATE -from ._helpers import _respond, _json_response, _csrf_field, _get_bookmark_token +from ._helpers import _respond, _redirect, _json_response, _csrf_field, _get_bookmark_token, _request_local from .subscriptions import _count_shared_pages +_flash = {} + + +def _set_flash(msg): + _flash[_request_local.csrf_token] = msg + + +def _get_flash(): + return _flash.pop(_request_local.csrf_token, "") + def handle_style_form(msg="", gateway_host="", scheme="http"): template = get_setting("custom_template") or DEFAULT_TEMPLATE @@ -21,7 +31,6 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): reranker = get_setting("use_reranker", "0") reranker_checked = " checked" if reranker == "1" else "" disabled = "" if semantic == "1" else " disabled" - dimmed = ' style="opacity:0.4"' if semantic != "1" else "" tcp_enabled = get_setting("tcp_enabled", "1") tcp_checked = " checked" if tcp_enabled == "1" else "" tcp_disabled = "" if tcp_enabled == "1" else " disabled" @@ -32,31 +41,65 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): lora_enabled = get_setting("lora_enabled", "0") lora_checked = " checked" if lora_enabled == "1" else "" lora_disabled = "" if lora_enabled == "1" else " disabled" - lora_dimmed = ' style="opacity:0.4"' if lora_enabled != "1" else "" lora_port = get_setting("lora_port", "") lora_frequency = get_setting("lora_frequency", "867200000") lora_bandwidth = get_setting("lora_bandwidth", "125000") lora_txpower = get_setting("lora_txpower", "7") lora_sf = get_setting("lora_sf", "8") lora_cr = get_setting("lora_cr", "5") + csrf = _csrf_field() from handlers import forum_plugin as _fp if _fp is not None: - forum_section = ( + forum_body = ( + f"
" + f'
' + f"{csrf}" + f'' f"

forum

" f'
" - f"Share URLs and discuss them with other TinyWeb instances. " - f"Requires tinyweb-forum — " - f'more info.

' + f"Share URLs and discuss them with other TinyWeb instances.

" + f'' + f"
" + f"
" ) + forum_nav = ' · forum' else: - forum_section = "" + forum_body = "" + forum_nav = "" + + msg = _get_flash() or msg + msg_html = "" + if msg: + msg_html = '

{msg}

'.format(msg=esc(msg)) + return _respond( f"

customize

" - f"

name your search engine

" - f'
' - f'{_csrf_field()}' - f'

' + f"{msg_html}" + f'' + f"
" + f"
" + f'' + f"{csrf}" + f'' + f"

site name

" + f'' + f' ' + f"" + f"
" + f"
" + f"
" + f'
' + f"{csrf}" + f'' f"

sharing

" f'
" @@ -66,39 +109,51 @@ def handle_style_form(msg="", gateway_host="", scheme="http"): f' share all pages except those tagged private
' f'
' - f'The private tag always excludes a page, even in public-only mode.' - f'' + f"" f'

' f'Currently sharing {shared_count} page(s). ' - f'preview what subscribers would see' - f'

' + f'preview' + f"

" + f'' + f"
" + f"
" + f"
" + f"{forum_body}" + f"
" + f"
" + f'
' + f"{csrf}" + f'' + f"

search

" + f'

" + f'

" + f'

" + f'manage semantic index' + f'
' + f"
" + f"
" + f"
" + f"
" + f'
' + f"{csrf}" + f'' f"

mesh network

" - f"

Choose how to connect to the mesh. You can enable both for maximum reach.

" + f"

Choose how to connect to the mesh.

" f"

internet

" - f'
" + f"
" + f"
" + f"

template

" + f"

Edit the full page template.

" f'
' - f'{_csrf_field()}' - f'
' - f'' + f"{csrf}" + f'

' + f'' f"
" - f"

bookmarklet

" - f"

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

" + f"
" + f"
" + f"
" + f"

tools

" + f"

bookmarklet

" + f"

Drag this link to your bookmarks bar.

" f'

r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}

' - f"

reset

" - f'
' - f'{_csrf_field()}' - f'' + f"

reset template

" + f'' + f"{csrf}" + f'' f"
" - f"

maintenance

" + f"

vacuum database

" f'
' - f'{_csrf_field()}' - f'' + f"{csrf}" + f'' f"
" - f"

{msg}

" + f"
" f'back', use_default=True, ) def handle_style_submit(body, gateway_host="", scheme="http"): - template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") - name = body.get("site_name", ["tinyweb"])[0].strip() - sharing = "1" if body.get("sharing_enabled") else "0" - sharing_mode = body.get("sharing_mode", ["exclude_private"])[0] - if sharing_mode not in ("exclude_private", "require_public"): - sharing_mode = "exclude_private" - set_setting("sharing_mode", sharing_mode) - semantic = "1" if body.get("semantic_search") else "0" - reranker = "1" if body.get("use_reranker") else "0" - compress = "1" if body.get("compress_embeddings") else "0" - tcp_enabled = "1" if body.get("tcp_enabled") else "0" - transport_host = body.get("transport_host", [""])[0].strip() - transport_port = body.get("transport_port", [""])[0].strip() - set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") - set_setting("site_name", name or "tinyweb") - set_setting("sharing_enabled", sharing) - set_setting("semantic_search", semantic) - set_setting("use_reranker", reranker) - set_setting("compress_embeddings", compress) - set_setting("tcp_enabled", tcp_enabled) - if transport_host: - set_setting("transport_host", transport_host) - if transport_port: - set_setting("transport_port", transport_port) - lora_enabled = "1" if body.get("lora_enabled") else "0" - set_setting("lora_enabled", lora_enabled) - set_setting("lora_port", body.get("lora_port", [""])[0].strip()) - set_setting("lora_frequency", body.get("lora_frequency", ["867200000"])[0].strip()) - set_setting("lora_bandwidth", body.get("lora_bandwidth", ["125000"])[0].strip()) - set_setting("lora_txpower", body.get("lora_txpower", ["7"])[0].strip()) - set_setting("lora_sf", body.get("lora_sf", ["8"])[0].strip()) - set_setting("lora_cr", body.get("lora_cr", ["5"])[0].strip()) - forum_enabled = "1" if body.get("forum_enabled") else "0" - current_forum = get_setting("forum_enabled", "0") - if forum_enabled != current_forum: - from handlers import forum_plugin - if forum_enabled == "1" and forum_plugin is None: - return handle_style_form( - "Forum plugin not installed. Run: pip install tinyweb-forum", - gateway_host=gateway_host, scheme=scheme, - ) - if forum_enabled == "1": - forum_plugin.enable() - try: - forum_plugin.fdb.set_setting("forum_enabled", "1") - except Exception: - pass - else: - forum_plugin.disable() - try: - forum_plugin.fdb.set_setting("forum_enabled", "0") - except Exception: - pass - set_setting("forum_enabled", forum_enabled) - templates_mod.FORUM_ENABLED = (forum_enabled == "1") - return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.", - gateway_host=gateway_host, scheme=scheme) + action = body.get("_action", [""])[0] + + if action == "name": + name = body.get("site_name", ["tinyweb"])[0].strip() + set_setting("site_name", name or "tinyweb") + _set_flash("Saved.") + return _redirect("/style") + + if action == "sharing": + sharing = "1" if body.get("sharing_enabled") else "0" + sharing_mode = body.get("sharing_mode", ["exclude_private"])[0] + if sharing_mode not in ("exclude_private", "require_public"): + sharing_mode = "exclude_private" + set_setting("sharing_mode", sharing_mode) + set_setting("sharing_enabled", sharing) + _set_flash("Saved.") + return _redirect("/style") + + if action == "forum": + forum_enabled = "1" if body.get("forum_enabled") else "0" + current_forum = get_setting("forum_enabled", "0") + if forum_enabled != current_forum: + from handlers import forum_plugin + if forum_enabled == "1" and forum_plugin is None: + _set_flash("Forum plugin not installed. Run: pip install tinyweb-forum") + return _redirect("/style") + if forum_enabled == "1": + forum_plugin.enable() + try: + forum_plugin.fdb.set_setting("forum_enabled", "1") + except Exception: + pass + else: + forum_plugin.disable() + try: + forum_plugin.fdb.set_setting("forum_enabled", "0") + except Exception: + pass + set_setting("forum_enabled", forum_enabled) + templates_mod.FORUM_ENABLED = (forum_enabled == "1") + _set_flash("Saved.") + return _redirect("/style") + + if action == "search": + semantic = "1" if body.get("semantic_search") else "0" + reranker = "1" if body.get("use_reranker") else "0" + compress = "1" if body.get("compress_embeddings") else "0" + set_setting("semantic_search", semantic) + set_setting("use_reranker", reranker) + set_setting("compress_embeddings", compress) + _set_flash("Saved.") + return _redirect("/style") + + if action == "mesh": + tcp_enabled = "1" if body.get("tcp_enabled") else "0" + transport_host = body.get("transport_host", [""])[0].strip() + transport_port = body.get("transport_port", [""])[0].strip() + set_setting("tcp_enabled", tcp_enabled) + if transport_host: + set_setting("transport_host", transport_host) + if transport_port: + set_setting("transport_port", transport_port) + lora_enabled = "1" if body.get("lora_enabled") else "0" + set_setting("lora_enabled", lora_enabled) + set_setting("lora_port", body.get("lora_port", [""])[0].strip()) + set_setting("lora_frequency", body.get("lora_frequency", ["867200000"])[0].strip()) + set_setting("lora_bandwidth", body.get("lora_bandwidth", ["125000"])[0].strip()) + set_setting("lora_txpower", body.get("lora_txpower", ["7"])[0].strip()) + set_setting("lora_sf", body.get("lora_sf", ["8"])[0].strip()) + set_setting("lora_cr", body.get("lora_cr", ["5"])[0].strip()) + _set_flash("Saved.") + return _redirect("/style") def handle_style_template_submit(body, gateway_host="", scheme="http"): template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") - return handle_style_form("Template saved.", gateway_host=gateway_host, scheme=scheme) + _set_flash("Template saved.") + return _redirect("/style") def handle_field_save(body): diff --git a/handlers/pages.py b/handlers/pages.py index cd3efba..0799251 100644 --- a/handlers/pages.py +++ b/handlers/pages.py @@ -172,7 +172,7 @@ def handle_pages(query=None): f'
  • {note_html}{tags_html} ' f'({esc(r["url"])}) ' - f'edit ' + f'edit ' f'remove
  • ' ) finally: @@ -282,7 +282,7 @@ def handle_bulk_action(body): return _redirect("/pages") -def handle_edit_form(page_id, msg=""): +def handle_edit_form(page_id, msg="", page=""): db = get_db() try: row = db.execute("SELECT id, url, title, body, note, summary FROM pages WHERE id = ?", (page_id,)).fetchone() @@ -292,12 +292,15 @@ def handle_edit_form(page_id, msg=""): finally: return_db(db) + back = "/pages" + ("?p=" + page if page else "") + return _respond( f"

    edit page

    " f"

    {esc(row['title'])}
    " f"{esc(row['url'])}

    " f'
    ' f'{_csrf_field()}' + f'' f'
    ' f'

    ' f'
    ' @@ -310,7 +313,7 @@ def handle_edit_form(page_id, msg=""): f'' f"
    " f"

    {msg}

    " - f'back' + f'back' ) @@ -335,7 +338,11 @@ def handle_edit_submit(page_id, body): finally: return_db(db) - return _redirect("/pages") + page = body.get("_page", [""])[0].strip() + target = "/pages" + if page: + target += "?p=" + page + return _redirect(target) def handle_delete_confirm(page_id): diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py index e0ecf68..97f20b2 100644 --- a/handlers/subscriptions.py +++ b/handlers/subscriptions.py @@ -162,7 +162,7 @@ def handle_subscriptions(msg=""): sync_btn = '' else: sync_btn = ( - f'
    ' + f'' f'{_csrf_field()}
    ' ) @@ -173,18 +173,22 @@ def handle_subscriptions(msg=""): f'
    last sync: {esc(last)}
    ' f'{status_html}' f'
    ' - f'browse' + f'browse' f'{sync_btn}' - f'
    ' + f'' f'{_csrf_field()}
    ' - f'
    ' + f'' f'{_csrf_field()}
    ' f'
    ' f'' ) + any_syncing = any( + s["id"] in _sync_threads and _sync_threads[s["id"]].is_alive() + for s in subs + ) + head_html = '' if any_syncing else "" listing = "" if subs: - any_syncing = any(sid in _sync_threads and _sync_threads[sid].is_alive() for sid in [s["id"] for s in subs]) syncall_btn = '' if any_syncing else '' listing = ( f'{cards}' @@ -201,7 +205,8 @@ def handle_subscriptions(msg=""): f'

    or subscribe to an instance

    ' f'

    {msg}

    ' f'
    {listing}' - f'
    back' + f'
    back', + head_html=head_html, ) diff --git a/templates.py b/templates.py index 89dadd7..27a1a6b 100644 --- a/templates.py +++ b/templates.py @@ -7,33 +7,6 @@ def esc(s): return html.escape(str(s)) -FORUM_CSS = """ -""" - def _nav_html(): name = esc(get_setting("site_name", "tinyweb")) forum_link = ' | forum' if FORUM_ENABLED else "" @@ -56,7 +29,7 @@ def _default_template(): ) -def wrap_page(body_html, use_default=False): +def wrap_page(body_html, use_default=False, head_html=""): if use_default: template = _default_template() else: @@ -67,8 +40,6 @@ def wrap_page(body_html, use_default=False): template = template.replace("{{forum_link}}", forum_link) template = template.replace("{{site_name}}", esc(get_setting("site_name", "tinyweb"))) template = template.replace("{{nav}}", _nav_html()) - # Inject forum layout CSS into for any template - head_end = "" - if head_end in template and FORUM_CSS not in template: - template = template.replace(head_end, FORUM_CSS + head_end) + if head_html: + template = template.replace("", head_html + "") return template.replace("{{content}}", body_html) diff --git a/themes/tinyweb-site.html b/themes/default.html similarity index 100% rename from themes/tinyweb-site.html rename to themes/default.html diff --git a/themes/kodama.html b/themes/kodama.html index 503ac9a..34fc125 100644 --- a/themes/kodama.html +++ b/themes/kodama.html @@ -1,747 +1,1424 @@ - - - - - - - - - - - - - -
    - -
    -
    - {{content}} -
    -
    -
    curated by hand · shared over mesh
    -
    -
    -
    - - + + + + + + + + + + + + +
    + +
    +
    + {{content}} +
    +
    +
    curated by hand · shared over mesh
    +
    +
    +
    + + \ No newline at end of file diff --git a/themes/kodama2.html b/themes/kodama2.html deleted file mode 100644 index 34fc125..0000000 --- a/themes/kodama2.html +++ /dev/null @@ -1,1424 +0,0 @@ - - - - - - - - - - - - -
    - -
    -
    - {{content}} -
    -
    -
    curated by hand · shared over mesh
    -
    -
    -
    - - - \ No newline at end of file From fc4e0c0b1d2dedff0dd8a51c9431ba11abe4474a Mon Sep 17 00:00:00 2001 From: blankie Date: Tue, 16 Jun 2026 05:18:39 +0000 Subject: [PATCH 14/14] junimo theme: use {{site_name}} and {{forum_link}} placeholders --- themes/junimo.html | 48 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/themes/junimo.html b/themes/junimo.html index f85e315..ee3fdcc 100644 --- a/themes/junimo.html +++ b/themes/junimo.html @@ -373,12 +373,25 @@ /* inputs */ input[type="text"], input[type="url"], + input[type="search"], + input:not([type]), input[name="q"], + input[name="title"], input[name="url"], - input[name="note"], input[name="tags"], + input[name="topics"], + input[name="instance"], + input[name="name"], + input[name="keywords"], + input[name="retention_days"], + input[name="note"], input[name="site_name"], - input[name="dest_hash"] { + input[name="dest_hash"], + input[name="manual_title"], + input[name="summary"], + input[name="transport_host"], + input[name="transport_port"], + textarea, select { background: rgba(20, 15, 50, 0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; @@ -547,6 +560,34 @@ hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; } + .forum-form input, .forum-form button, .forum-toolbar input { border-radius: 4px; } + .forum-actions a, a.forum-action, a.forum-action-inline { + border: 1px solid rgba(255,255,255,0.12); padding: 6px 14px; text-transform: uppercase; font-size: 13px; background: rgba(30,20,60,0.7); color: #a098b0; + } + .forum-actions a:hover, a.forum-action:hover, a.forum-action-inline:hover { + background: rgba(50,35,90,0.7); color: #d0c0e0; border-color: rgba(240,216,120,0.3); + } + a.forum-action-inline { text-transform: none; font-size: 13px; padding: 2px 6px; border: none; background: none; } + .forum-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0.5rem 0; } + .forum-toolbar form { flex: 1; min-width: 160px; margin: 0; } + .forum-toolbar input[name=q] { width: 100%; box-sizing: border-box; padding: 6px 10px; } + .forum-toolbar-actions { display: flex; flex-wrap: wrap; gap: 4px; } + .section { margin: 1.5rem 0; } + .section-title { font-weight: 600; margin-bottom: 0.3rem; color: #d0c0a0; } + .section-desc { font-size: 0.85rem; color: #5a5070; margin-bottom: 0.5rem; } + .section ul { margin: 0.3rem 0; } + .forum-form input, .forum-form textarea, .forum-form button { margin-bottom: 8px; } + .forum-form small { display: block; margin-bottom: 6px; } + .forum-form label { display: block; margin-bottom: 6px; } + .forum-form + .forum-form { margin-top: 1rem; } + .forum-form + .section-title { margin-top: 1rem; } + .section-desc + .forum-form { margin-top: 0.8rem; } + ul + .forum-form { margin-top: 1rem; } + .checkbox-label { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; } + .forum-status { font-size: 0.82rem; color: #5a5070; margin: 0 0 0.8rem 0; } + .forum-status span { margin-right: 1.2rem; } + .forum-nav { margin: 1rem 0; } + small { font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace; font-size: 0.7rem; @@ -615,11 +656,12 @@