remove demo pages, rename themes, add Content-Length header

This commit is contained in:
blankie 2026-06-16 05:11:37 +00:00
parent a72de2bb10
commit 7a81b519bb
10 changed files with 1652 additions and 2370 deletions

View file

@ -165,12 +165,15 @@ class GatewayHandler(BaseHTTPRequestHandler):
"style-src 'self' 'unsafe-inline'; " "style-src 'self' 'unsafe-inline'; "
"script-src 'self' 'unsafe-inline'; " "script-src 'self' 'unsafe-inline'; "
"img-src 'self' data:") "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(): for k, v in resp.get("headers", {}).items():
self.send_header(k, v) self.send_header(k, v)
self.end_headers() self.end_headers()
resp_body = resp.get("body", "") if encoded:
if resp_body: self.wfile.write(encoded)
self.wfile.write(resp_body.encode() if isinstance(resp_body, str) else resp_body)
except ConnectionError as e: except ConnectionError as e:
GatewayState.link = None GatewayState.link = None

View file

@ -32,7 +32,7 @@ from .subscriptions import (
handle_subscription_delete, handle_subscription_syncall, handle_subscription_delete, handle_subscription_syncall,
_sync_threads, _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 .tags import handle_tags, handle_tag_browse
from .data import ( from .data import (
handle_export, handle_import_form, handle_import_submit, handle_export, handle_import_form, handle_import_submit,
@ -66,7 +66,7 @@ def _dispatch_inner(data):
return handle_pages(query) return handle_pages(query)
elif path.startswith("/edit/"): elif path.startswith("/edit/"):
pid = extract_id("/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/"): elif path.startswith("/delete/"):
pid = extract_id("/delete/") pid = extract_id("/delete/")
return handle_delete_confirm(pid) if pid is not None else _error(400) 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) return handle_field_save(body)
elif path == "/style/reset": elif path == "/style/reset":
set_setting("custom_template", "") 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": elif path == "/style/vacuum":
from db import vacuum_db from db import vacuum_db
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": elif path == "/import":
return handle_import_submit(body) return handle_import_submit(body)
elif path == "/reindex": elif path == "/reindex":

View file

@ -65,11 +65,11 @@ def _get_bookmark_token():
return token return token
def _respond(body_html, status=200, use_default=False): def _respond(body_html, status=200, use_default=False, head_html=""):
return { return {
"status": status, "status": status,
"content_type": "text/html; charset=utf-8", "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": {}, "headers": {},
} }

View file

@ -1,9 +1,19 @@
from db import get_db, return_db, get_setting, set_setting, get_site_name from db import get_db, return_db, get_setting, set_setting, get_site_name
import templates as templates_mod import templates as templates_mod
from templates import esc, DEFAULT_TEMPLATE 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 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"): def handle_style_form(msg="", gateway_host="", scheme="http"):
template = get_setting("custom_template") or DEFAULT_TEMPLATE 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 = get_setting("use_reranker", "0")
reranker_checked = " checked" if reranker == "1" else "" reranker_checked = " checked" if reranker == "1" else ""
disabled = "" if semantic == "1" else " disabled" disabled = "" if semantic == "1" else " disabled"
dimmed = ' style="opacity:0.4"' if semantic != "1" else ""
tcp_enabled = get_setting("tcp_enabled", "1") tcp_enabled = get_setting("tcp_enabled", "1")
tcp_checked = " checked" if tcp_enabled == "1" else "" tcp_checked = " checked" if tcp_enabled == "1" else ""
tcp_disabled = "" if tcp_enabled == "1" else " disabled" 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_enabled = get_setting("lora_enabled", "0")
lora_checked = " checked" if lora_enabled == "1" else "" lora_checked = " checked" if lora_enabled == "1" else ""
lora_disabled = "" if lora_enabled == "1" else " disabled" 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_port = get_setting("lora_port", "")
lora_frequency = get_setting("lora_frequency", "867200000") lora_frequency = get_setting("lora_frequency", "867200000")
lora_bandwidth = get_setting("lora_bandwidth", "125000") lora_bandwidth = get_setting("lora_bandwidth", "125000")
lora_txpower = get_setting("lora_txpower", "7") lora_txpower = get_setting("lora_txpower", "7")
lora_sf = get_setting("lora_sf", "8") lora_sf = get_setting("lora_sf", "8")
lora_cr = get_setting("lora_cr", "5") lora_cr = get_setting("lora_cr", "5")
csrf = _csrf_field()
from handlers import forum_plugin as _fp from handlers import forum_plugin as _fp
if _fp is not None: if _fp is not None:
forum_section = ( forum_body = (
f"<section id=\"forum\">"
f'<form method="post" action="/style">'
f"{csrf}"
f'<input type="hidden" name="_action" value="forum">'
f"<h2>forum</h2>" f"<h2>forum</h2>"
f'<label><input type="checkbox" name="forum_enabled" value="1"{forum_checked}>' f'<label><input type="checkbox" name="forum_enabled" value="1"{forum_checked}>'
f" enable forum (shared URL discussion board)</label><br>" f" enable forum (shared URL discussion board)</label><br>"
f"<small>Share URLs and discuss them with other TinyWeb instances. " f"<small>Share URLs and discuss them with other TinyWeb instances.</small><br><br>"
f"Requires <code>tinyweb-forum</code> — " f'<input type="submit" value="save forum">'
f'<a href="https://codeberg.org/tinyweb/tinyweb-forum">more info</a>.</small><br><br>' f"</form>"
f"</section>"
) )
forum_nav = ' · <a href="#forum">forum</a>'
else: else:
forum_section = "" forum_body = ""
forum_nav = ""
msg = _get_flash() or msg
msg_html = ""
if msg:
msg_html = '<p><em>{msg}</em></p>'.format(msg=esc(msg))
return _respond( return _respond(
f"<h1>customize</h1>" f"<h1>customize</h1>"
f"<h2>name your search engine</h2>" f"{msg_html}"
f'<form method="post" action="/style" id="settings">' f'<nav>'
f'{_csrf_field()}' f'<a href="#site-name">site name</a>'
f'<input name="site_name" value="{esc(name)}" placeholder="tinyweb" size="30"><br><br>' f' · <a href="#sharing">sharing</a>'
f'{forum_nav}'
f' · <a href="#search">search</a>'
f' · <a href="#mesh">mesh</a>'
f' · <a href="#template">template</a>'
f' · <a href="#tools">tools</a>'
f'</nav>'
f"<hr>"
f"<section id=\"site-name\">"
f'<form method="post" action="/style">'
f"{csrf}"
f'<input type="hidden" name="_action" value="name">'
f"<h2>site name</h2>"
f'<input name="site_name" value="{esc(name)}" placeholder="tinyweb" size="30">'
f' <input type="submit" value="save name">'
f"</form>"
f"</section>"
f"<hr>"
f"<section id=\"sharing\">"
f'<form method="post" action="/style">'
f"{csrf}"
f'<input type="hidden" name="_action" value="sharing">'
f"<h2>sharing</h2>" f"<h2>sharing</h2>"
f'<label><input type="checkbox" name="sharing_enabled" value="1"{checked}>' f'<label><input type="checkbox" name="sharing_enabled" value="1"{checked}>'
f" share your site list publicly at /api/sites</label><br>" f" share your site list publicly at /api/sites</label><br>"
@ -66,39 +109,51 @@ def handle_style_form(msg="", gateway_host="", scheme="http"):
f' share all pages except those tagged <code>private</code></label><br>' f' share all pages except those tagged <code>private</code></label><br>'
f'<label><input type="radio" name="sharing_mode" value="require_public"{require_checked}>' f'<label><input type="radio" name="sharing_mode" value="require_public"{require_checked}>'
f' share only pages tagged <code>public</code></label><br>' f' share only pages tagged <code>public</code></label><br>'
f'<small>The <code>private</code> tag always excludes a page, even in public-only mode.</small>' f"</div>"
f'</div>'
f'<p style="margin-top:0.6rem">' f'<p style="margin-top:0.6rem">'
f'Currently sharing <b>{shared_count}</b> page(s). ' f'Currently sharing <b>{shared_count}</b> page(s). '
f'<a href="/share/preview">preview what subscribers would see</a>' f'<a href="/share/preview">preview</a>'
f'</p>' f"</p>"
f'<input type="submit" value="save sharing">'
f"</form>"
f"</section>"
f"<hr>"
f"{forum_body}"
f"<hr>"
f"<section id=\"search\">"
f'<form method="post" action="/style">'
f"{csrf}"
f'<input type="hidden" name="_action" value="search">'
f"<h2>search</h2>"
f'<label><input type="checkbox" name="semantic_search" value="1"{semantic_checked}>'
f" semantic search</label><br><br>"
f'<label><input type="checkbox" name="use_reranker" value="1"{reranker_checked}{disabled}>'
f" cross-encoder reranking</label><br><br>"
f'<label><input type="checkbox" name="compress_embeddings" value="1"{compress_checked}{disabled}>'
f" compress embeddings</label><br><br>"
f'<a href="/reindex">manage semantic index</a>'
f'<br><input type="submit" value="save search">'
f"</form>"
f"</section>"
f"<hr>"
f"<section id=\"mesh\">"
f'<form method="post" action="/style">'
f"{csrf}"
f'<input type="hidden" name="_action" value="mesh">'
f"<h2>mesh network</h2>" f"<h2>mesh network</h2>"
f"<p>Choose how to connect to the mesh. You can enable both for maximum reach.</p>" f"<p>Choose how to connect to the mesh.</p>"
f"<h3>internet</h3>" f"<h3>internet</h3>"
f'<label><input type="checkbox" name="tcp_enabled" value="1"{tcp_checked} ' f'<label><input type="checkbox" name="tcp_enabled" value="1"{tcp_checked}>'
f'onchange="var d=!this.checked;'
f'for(var e of document.querySelectorAll(\'#tcp-fields input\'))e.disabled=d;'
f'document.getElementById(\'tcp-fields\').style.opacity=d?\'0.4\':\'1\'">'
f" connect via internet transport node</label><br>" f" connect via internet transport node</label><br>"
f"<small>Reach peers anywhere online.</small><br>"
f'<div id="tcp-fields" style="margin-top:0.5rem{";opacity:0.4" if tcp_enabled != "1" else ""}">'
f"<small>Default: rnode.bre.land:4242</small><br>" f"<small>Default: rnode.bre.land:4242</small><br>"
f'<input name="transport_host" value="{esc(transport_host)}" placeholder="hostname" size="30"{tcp_disabled}>' f'<input name="transport_host" value="{esc(transport_host)}" size="30"{tcp_disabled}>'
f' <input name="transport_port" value="{esc(transport_port)}" placeholder="port" size="6"{tcp_disabled}><br>' f' <input name="transport_port" value="{esc(transport_port)}" size="6"{tcp_disabled}><br>'
f'<p><a href="https://rmap.world/" target="_blank" rel="noreferrer noopener">discover more nodes</a></p>'
f'</div><br>'
f"<h3>LoRa</h3>" f"<h3>LoRa</h3>"
f'<label><input type="checkbox" name="lora_enabled" value="1"{lora_checked} ' f'<label><input type="checkbox" name="lora_enabled" value="1"{lora_checked}>'
f'onchange="var d=!this.checked;document.getElementById(\'lora-port\').disabled=d;'
f'document.getElementById(\'lora-extras\').style.opacity=d?\'0.4\':\'1\';'
f'for(var e of document.querySelectorAll(\'#lora-extras input\'))e.disabled=d">'
f" connect via LoRa radio</label><br>" f" connect via LoRa radio</label><br>"
f"<small>Reach nearby peers off-grid with an <a href=\"https://unsigned.io/rnode/\" target=\"_blank\" rel=\"noreferrer noopener\">RNode</a>.</small><br><br>"
f'<div id="lora-fields" style="{";opacity:0.4" if lora_enabled != "1" else ""}">'
f'<label>Serial port: <input id="lora-port" name="lora_port" value="{esc(lora_port)}" ' f'<label>Serial port: <input id="lora-port" name="lora_port" value="{esc(lora_port)}" '
f'placeholder="/dev/ttyUSB0" size="20"{lora_disabled}></label><br><br>' f'placeholder="/dev/ttyUSB0" size="20"{lora_disabled}></label><br>'
f'<details><summary>advanced radio settings</summary>' f"<details><summary>advanced radio settings</summary>"
f'<div id="lora-extras" style="margin-top:0.5rem">'
f'<label>Frequency (Hz): <input name="lora_frequency" value="{esc(lora_frequency)}" size="12"{lora_disabled}></label><br>' f'<label>Frequency (Hz): <input name="lora_frequency" value="{esc(lora_frequency)}" size="12"{lora_disabled}></label><br>'
f"<small>ISM band frequency. Default: 867200000 (868 MHz EU). US: 915000000.</small><br><br>" f"<small>ISM band frequency. Default: 867200000 (868 MHz EU). US: 915000000.</small><br><br>"
f'<label>Bandwidth (Hz): <input name="lora_bandwidth" value="{esc(lora_bandwidth)}" size="8"{lora_disabled}></label><br>' f'<label>Bandwidth (Hz): <input name="lora_bandwidth" value="{esc(lora_bandwidth)}" size="8"{lora_disabled}></label><br>'
@ -108,137 +163,123 @@ def handle_style_form(msg="", gateway_host="", scheme="http"):
f'<label>Spreading Factor: <input name="lora_sf" value="{esc(lora_sf)}" size="4"{lora_disabled}></label><br>' f'<label>Spreading Factor: <input name="lora_sf" value="{esc(lora_sf)}" size="4"{lora_disabled}></label><br>'
f"<small>5-12. Higher = longer range, slower speed.</small><br><br>" f"<small>5-12. Higher = longer range, slower speed.</small><br><br>"
f'<label>Coding Rate: <input name="lora_cr" value="{esc(lora_cr)}" size="4"{lora_disabled}></label><br>' f'<label>Coding Rate: <input name="lora_cr" value="{esc(lora_cr)}" size="4"{lora_disabled}></label><br>'
f"<small>5-8. Higher = more error correction.</small><br>" f"<small>5-8. Higher = more error correction.</small>"
f'</div></details></div><br>' f"</details>"
f"<h2>search</h2>" f'<br><input type="submit" value="save mesh">'
f"<h3>ai</h3>"
f'<label><input type="checkbox" name="semantic_search" value="1"{semantic_checked} '
f'onchange="var d=!this.checked;document.getElementById(\'reranker\').disabled=d;'
f'document.getElementById(\'ai-extras\').style.opacity=d?\'0.4\':\'1\'">'
f" semantic search (similarity matching)</label><br>"
f"<small>Requires onnxruntime, tokenizers, hnswlib. Downloads ~30MB of models on first use.</small><br><br>"
f'<div id="ai-extras"{dimmed}>'
f'<label><input type="checkbox" id="reranker" name="use_reranker" value="1"{reranker_checked}{disabled}>'
f" cross-encoder reranking (more accurate)</label><br>"
f"<small>Uses a 22MB model. Adds ~50ms per search. Disable for faster results.</small><br><br>"
f'<label><input type="checkbox" name="compress_embeddings" value="1"{compress_checked}{disabled}>'
f" compress embeddings (50% storage savings)</label><br>"
f"<small>Saves ~50% on storage for embeddings. Slight quality reduction at large scale.</small><br><br>"
f'<a href="/reindex">manage semantic index</a><br><br>'
f"</div>"
f"{forum_section}"
f'<p id="settings-status" style="font-size:0.85rem;color:#888;min-height:1.2em"></p>'
f'<noscript><button type="submit">save settings</button></noscript>'
f"</form>" f"</form>"
'<script>' f"</section>"
'(function(){' f"<hr>"
"var f=document.getElementById('settings');" f"<section id=\"template\">"
"var csrf=f.querySelector('input[name=_csrf]').value;" f"<h2>template</h2>"
"var st=document.getElementById('settings-status');" f"<p>Edit the full page template.</p>"
"f.querySelectorAll('input:not([type=hidden]):not([type=submit]),select').forEach(function(el){"
"el.addEventListener('change',function(){"
"var key=el.name,val=el.type=='checkbox'?(el.checked?'1':'0'):el.value;"
"st.textContent='saving...';"
"fetch('/style/field',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
"body:'_csrf='+encodeURIComponent(csrf)+'&key='+encodeURIComponent(key)+'&value='+encodeURIComponent(val)})"
".then(function(r){return r.json()})"
".then(function(d){st.textContent=d.status=='ok'?'saved \\u2713':d.message||'error';setTimeout(function(){st.textContent=''},3000)})"
".catch(function(){st.textContent='save failed';setTimeout(function(){st.textContent=''},3000)});"
"},false);});"
'})();'
'</script>'
f"<h2>custom html</h2>"
f"<p>Edit the full page template. Use <code>{esc('{{content}}')}</code> "
f"where page content should appear.</p>"
f'<form method="post" action="/style/template">' f'<form method="post" action="/style/template">'
f'{_csrf_field()}' f"{csrf}"
f'<textarea name="template" rows="20" cols="60">{esc(template)}</textarea><br>' f'<textarea name="template" rows="12" cols="60" style="width:100%">{esc(template)}</textarea><br><br>'
f'<button type="submit">save template</button>' f'<input type="submit" value="save template">'
f"</form>" f"</form>"
f"<h2>bookmarklet</h2>" f"</section>"
f"<p>Drag this link to your bookmarks bar. Click it on any page to index it instantly.</p>" f"<hr>"
f"<section id=\"tools\">"
f"<h2>tools</h2>"
f"<h3>bookmarklet</h3>"
f"<p>Drag this link to your bookmarks bar.</p>"
f'<p><a href="javascript:void(fetch(\'{esc(scheme)}://{esc(gateway_host or "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'<p><a href="javascript:void(fetch(\'{esc(scheme)}://{esc(gateway_host or "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"<h3>reset template</h3>"
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}"
f'{_csrf_field()}' f'<input type="submit" value="reset to default">'
f'<button type="submit">reset template to default</button>'
f"</form>" f"</form>"
f"<h2>maintenance</h2>" f"<h3>vacuum database</h3>"
f'<form method="post" action="/style/vacuum">' f'<form method="post" action="/style/vacuum">'
f'{_csrf_field()}' f"{csrf}"
f'<button type="submit">vacuum database</button>' f'<input type="submit" value="vacuum">'
f"</form>" f"</form>"
f"<p>{msg}</p>" f"</section>"
f'<a href="/">back</a>', f'<a href="/">back</a>',
use_default=True, use_default=True,
) )
def handle_style_submit(body, gateway_host="", scheme="http"): def handle_style_submit(body, gateway_host="", scheme="http"):
template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") action = body.get("_action", [""])[0]
name = body.get("site_name", ["tinyweb"])[0].strip()
sharing = "1" if body.get("sharing_enabled") else "0" if action == "name":
sharing_mode = body.get("sharing_mode", ["exclude_private"])[0] name = body.get("site_name", ["tinyweb"])[0].strip()
if sharing_mode not in ("exclude_private", "require_public"): set_setting("site_name", name or "tinyweb")
sharing_mode = "exclude_private" _set_flash("Saved.")
set_setting("sharing_mode", sharing_mode) return _redirect("/style")
semantic = "1" if body.get("semantic_search") else "0"
reranker = "1" if body.get("use_reranker") else "0" if action == "sharing":
compress = "1" if body.get("compress_embeddings") else "0" sharing = "1" if body.get("sharing_enabled") else "0"
tcp_enabled = "1" if body.get("tcp_enabled") else "0" sharing_mode = body.get("sharing_mode", ["exclude_private"])[0]
transport_host = body.get("transport_host", [""])[0].strip() if sharing_mode not in ("exclude_private", "require_public"):
transport_port = body.get("transport_port", [""])[0].strip() sharing_mode = "exclude_private"
set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") set_setting("sharing_mode", sharing_mode)
set_setting("site_name", name or "tinyweb") set_setting("sharing_enabled", sharing)
set_setting("sharing_enabled", sharing) _set_flash("Saved.")
set_setting("semantic_search", semantic) return _redirect("/style")
set_setting("use_reranker", reranker)
set_setting("compress_embeddings", compress) if action == "forum":
set_setting("tcp_enabled", tcp_enabled) forum_enabled = "1" if body.get("forum_enabled") else "0"
if transport_host: current_forum = get_setting("forum_enabled", "0")
set_setting("transport_host", transport_host) if forum_enabled != current_forum:
if transport_port: from handlers import forum_plugin
set_setting("transport_port", transport_port) if forum_enabled == "1" and forum_plugin is None:
lora_enabled = "1" if body.get("lora_enabled") else "0" _set_flash("Forum plugin not installed. Run: pip install tinyweb-forum")
set_setting("lora_enabled", lora_enabled) return _redirect("/style")
set_setting("lora_port", body.get("lora_port", [""])[0].strip()) if forum_enabled == "1":
set_setting("lora_frequency", body.get("lora_frequency", ["867200000"])[0].strip()) forum_plugin.enable()
set_setting("lora_bandwidth", body.get("lora_bandwidth", ["125000"])[0].strip()) try:
set_setting("lora_txpower", body.get("lora_txpower", ["7"])[0].strip()) forum_plugin.fdb.set_setting("forum_enabled", "1")
set_setting("lora_sf", body.get("lora_sf", ["8"])[0].strip()) except Exception:
set_setting("lora_cr", body.get("lora_cr", ["5"])[0].strip()) pass
forum_enabled = "1" if body.get("forum_enabled") else "0" else:
current_forum = get_setting("forum_enabled", "0") forum_plugin.disable()
if forum_enabled != current_forum: try:
from handlers import forum_plugin forum_plugin.fdb.set_setting("forum_enabled", "0")
if forum_enabled == "1" and forum_plugin is None: except Exception:
return handle_style_form( pass
"Forum plugin not installed. Run: pip install tinyweb-forum", set_setting("forum_enabled", forum_enabled)
gateway_host=gateway_host, scheme=scheme, templates_mod.FORUM_ENABLED = (forum_enabled == "1")
) _set_flash("Saved.")
if forum_enabled == "1": return _redirect("/style")
forum_plugin.enable()
try: if action == "search":
forum_plugin.fdb.set_setting("forum_enabled", "1") semantic = "1" if body.get("semantic_search") else "0"
except Exception: reranker = "1" if body.get("use_reranker") else "0"
pass compress = "1" if body.get("compress_embeddings") else "0"
else: set_setting("semantic_search", semantic)
forum_plugin.disable() set_setting("use_reranker", reranker)
try: set_setting("compress_embeddings", compress)
forum_plugin.fdb.set_setting("forum_enabled", "0") _set_flash("Saved.")
except Exception: return _redirect("/style")
pass
set_setting("forum_enabled", forum_enabled) if action == "mesh":
templates_mod.FORUM_ENABLED = (forum_enabled == "1") tcp_enabled = "1" if body.get("tcp_enabled") else "0"
return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.", transport_host = body.get("transport_host", [""])[0].strip()
gateway_host=gateway_host, scheme=scheme) 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"): def handle_style_template_submit(body, gateway_host="", scheme="http"):
template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n") template = body.get("template", [""])[0].replace("\r\n", "\n").replace("\r", "\n")
set_setting("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "") 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): def handle_field_save(body):

View file

@ -172,7 +172,7 @@ def handle_pages(query=None):
f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> ' f'<li><label><input type="checkbox" name="ids" value="{r["id"]}"> '
f'{esc(r["title"])}</label>{note_html}{tags_html} ' f'{esc(r["title"])}</label>{note_html}{tags_html} '
f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> ' f'<small>(<a href="{esc(r["url"])}" rel="noreferrer noopener">{esc(r["url"])}</a>)</small> '
f'<a href="/edit/{r["id"]}">edit</a> ' f'<a href="/edit/{r["id"]}?p={page}">edit</a> '
f'<a href="/delete/{r["id"]}">remove</a></li>' f'<a href="/delete/{r["id"]}">remove</a></li>'
) )
finally: finally:
@ -282,7 +282,7 @@ def handle_bulk_action(body):
return _redirect("/pages") return _redirect("/pages")
def handle_edit_form(page_id, msg=""): def handle_edit_form(page_id, msg="", page=""):
db = get_db() db = get_db()
try: try:
row = db.execute("SELECT id, url, title, body, note, summary FROM pages WHERE id = ?", (page_id,)).fetchone() 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: finally:
return_db(db) return_db(db)
back = "/pages" + ("?p=" + page if page else "")
return _respond( return _respond(
f"<h1>edit page</h1>" f"<h1>edit page</h1>"
f"<p><b>{esc(row['title'])}</b><br>" f"<p><b>{esc(row['title'])}</b><br>"
f"<small>{esc(row['url'])}</small></p>" f"<small>{esc(row['url'])}</small></p>"
f'<form method="post" action="/edit/{row["id"]}">' f'<form method="post" action="/edit/{row["id"]}">'
f'{_csrf_field()}' f'{_csrf_field()}'
f'<input type="hidden" name="_page" value="{esc(page)}">'
f'<label>Title:</label><br>' f'<label>Title:</label><br>'
f'<input name="title" value="{esc(row["title"])}" size="60"><br><br>' f'<input name="title" value="{esc(row["title"])}" size="60"><br><br>'
f'<label>Summary (shown in search results):</label><br>' f'<label>Summary (shown in search results):</label><br>'
@ -310,7 +313,7 @@ def handle_edit_form(page_id, msg=""):
f'<button type="submit">save</button>' f'<button type="submit">save</button>'
f"</form>" f"</form>"
f"<p>{msg}</p>" f"<p>{msg}</p>"
f'<a href="/pages">back</a>' f'<a href="{back}">back</a>'
) )
@ -335,7 +338,11 @@ def handle_edit_submit(page_id, body):
finally: finally:
return_db(db) 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): def handle_delete_confirm(page_id):

View file

@ -162,7 +162,7 @@ def handle_subscriptions(msg=""):
sync_btn = '<button disabled>syncing...</button>' sync_btn = '<button disabled>syncing...</button>'
else: else:
sync_btn = ( sync_btn = (
f'<form method="post" action="/subscriptions/sync/{sub_id}" style="display:inline">' f'<form method="post" action="/subscriptions/sync/{sub_id}" style="display:inline-block;margin:0">'
f'{_csrf_field()}<button>sync now</button></form>' f'{_csrf_field()}<button>sync now</button></form>'
) )
@ -173,18 +173,22 @@ def handle_subscriptions(msg=""):
f'<div style="margin-top:0.4rem;font-size:0.85rem;color:#606060">last sync: {esc(last)}</div>' f'<div style="margin-top:0.4rem;font-size:0.85rem;color:#606060">last sync: {esc(last)}</div>'
f'{status_html}' f'{status_html}'
f'<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-top:0.7rem">' f'<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;margin-top:0.7rem">'
f'<a href="/subscriptions/browse/{sub_id}">browse</a>' f'<a href="/subscriptions/browse/{sub_id}" style="display:inline-flex;align-items:center;padding:0.3em 0">browse</a>'
f'{sync_btn}' f'{sync_btn}'
f'<form method="post" action="/subscriptions/autosync/{sub_id}" style="display:inline">' f'<form method="post" action="/subscriptions/autosync/{sub_id}" style="display:inline-block;margin:0">'
f'{_csrf_field()}<button>auto-sync: {auto_label}</button></form>' f'{_csrf_field()}<button>auto-sync: {auto_label}</button></form>'
f'<form method="post" action="/subscriptions/delete/{sub_id}" style="display:inline">' f'<form method="post" action="/subscriptions/delete/{sub_id}" style="display:inline-block;margin:0">'
f'{_csrf_field()}<button>remove</button></form>' f'{_csrf_field()}<button>remove</button></form>'
f'</div>' f'</div>'
f'</div>' f'</div>'
) )
any_syncing = any(
s["id"] in _sync_threads and _sync_threads[s["id"]].is_alive()
for s in subs
)
head_html = '<meta http-equiv="refresh" content="3">' if any_syncing else ""
listing = "" listing = ""
if subs: 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 = '<button disabled>syncing...</button>' if any_syncing else '<button>sync all</button>' syncall_btn = '<button disabled>syncing...</button>' if any_syncing else '<button>sync all</button>'
listing = ( listing = (
f'{cards}' f'{cards}'
@ -201,7 +205,8 @@ def handle_subscriptions(msg=""):
f'<p><small>or <a href="/subscriptions/add">subscribe to an instance</a></small></p>' f'<p><small>or <a href="/subscriptions/add">subscribe to an instance</a></small></p>'
f'<p>{msg}</p>' f'<p>{msg}</p>'
f'<hr>{listing}' f'<hr>{listing}'
f'<br><a href="/">back</a>' f'<br><a href="/">back</a>',
head_html=head_html,
) )

View file

@ -7,33 +7,6 @@ def esc(s):
return html.escape(str(s)) return html.escape(str(s))
FORUM_CSS = """
<style>
.forum-form { max-width: 500px; }
.forum-form input:not([type=checkbox]):not([type=radio]), .forum-form textarea { width: 100%; box-sizing: border-box; }
.forum-form textarea { resize: vertical; }
.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-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.forum-toolbar form { flex: 1; min-width: 160px; margin: 0; }
.forum-toolbar input[name=q] { width: 100%; box-sizing: border-box; }
.forum-toolbar-actions { display: flex; flex-wrap: wrap; gap: 4px; }
.forum-list { list-style: none; padding: 0; }
.forum-status { margin: 0 0 0.8rem 0; }
.forum-status span { margin-right: 0.5rem; }
.forum-nav { margin: 0.5rem 0; }
.section { margin: 1.5rem 0; }
.section-title { font-weight: 600; margin-bottom: 0.3rem; }
.section-desc { font-size: 0.85rem; margin-bottom: 0.5rem; }
.section ul { margin: 0.3rem 0; }
</style>"""
def _nav_html(): def _nav_html():
name = esc(get_setting("site_name", "tinyweb")) name = esc(get_setting("site_name", "tinyweb"))
forum_link = ' | <a href="/forum">forum</a>' if FORUM_ENABLED else "" forum_link = ' | <a href="/forum">forum</a>' 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: if use_default:
template = _default_template() template = _default_template()
else: else:
@ -67,8 +40,6 @@ def wrap_page(body_html, use_default=False):
template = template.replace("{{forum_link}}", forum_link) template = template.replace("{{forum_link}}", forum_link)
template = template.replace("{{site_name}}", esc(get_setting("site_name", "tinyweb"))) template = template.replace("{{site_name}}", esc(get_setting("site_name", "tinyweb")))
template = template.replace("{{nav}}", _nav_html()) template = template.replace("{{nav}}", _nav_html())
# Inject forum layout CSS into <head> for any template if head_html:
head_end = "</head>" template = template.replace("</head>", head_html + "</head>")
if head_end in template and FORUM_CSS not in template:
template = template.replace(head_end, FORUM_CSS + head_end)
return template.replace("{{content}}", body_html) return template.replace("{{content}}", body_html)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff