Compare commits
14 commits
6c4b26781d
...
fc4e0c0b1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc4e0c0b1d | ||
|
|
7a81b519bb | ||
|
|
a72de2bb10 | ||
|
|
076390cbee | ||
|
|
117fc1312c | ||
|
|
d97ba6d489 | ||
|
|
501d9e1838 | ||
|
|
1461c62c91 | ||
|
|
27147bd3f9 | ||
|
|
4a45700067 | ||
|
|
0da46d29fc | ||
|
|
25e24efbfa | ||
|
|
e0a5d44d94 | ||
|
|
8e68d02413 |
17 changed files with 1764 additions and 2367 deletions
|
|
@ -13,9 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN mkdir -p /data \
|
RUN mkdir -p /data
|
||||||
&& ln -sf /data/index.db index.db \
|
|
||||||
&& ln -sf /data/tinyweb_identity tinyweb_identity
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,14 +105,6 @@ tmux new-session -d -s tinyweb 'python app.py'
|
||||||
nohup 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
|
### Docker
|
||||||
|
|
||||||
A `docker-compose.yml` is included for containerized setups. Build and run:
|
A `docker-compose.yml` is included for containerized setups. Build and run:
|
||||||
|
|
|
||||||
2
app.py
2
app.py
|
|
@ -16,7 +16,7 @@ from gateway import GatewayState, GatewayHandler
|
||||||
IDENTITY_FILE = "tinyweb_identity"
|
IDENTITY_FILE = "tinyweb_identity"
|
||||||
DEFAULT_TRANSPORT_HOST = "rnode.bre.land"
|
DEFAULT_TRANSPORT_HOST = "rnode.bre.land"
|
||||||
DEFAULT_TRANSPORT_PORT = 4242
|
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():
|
def get_transport_config():
|
||||||
|
|
|
||||||
2
db.py
2
db.py
|
|
@ -6,7 +6,7 @@ import os
|
||||||
from urllib.parse import urlparse, urljoin, parse_qs, urlencode, urlunparse, quote
|
from urllib.parse import urlparse, urljoin, parse_qs, urlencode, urlunparse, quote
|
||||||
from bs4 import BeautifulSoup
|
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")
|
DATABASE = os.path.join(DATA_DIR, "index.db")
|
||||||
|
|
||||||
BLOCKED_NETWORKS = [
|
BLOCKED_NETWORKS = [
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ if [ ! -f "$CONFIG_FILE" ]; then
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export TINYWEB_DATA_DIR="/data"
|
||||||
export RNS_CONFIG_DIR="$CONFIG_DIR"
|
export RNS_CONFIG_DIR="$CONFIG_DIR"
|
||||||
# Bind to 0.0.0.0 inside the container; isolation is handled by Docker's port mapping.
|
# 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 "$@"
|
exec python app.py --bind 0.0.0.0 "$@"
|
||||||
|
|
|
||||||
10
gateway.py
10
gateway.py
|
|
@ -125,6 +125,7 @@ class GatewayHandler(BaseHTTPRequestHandler):
|
||||||
"body": body,
|
"body": body,
|
||||||
"cookies": cookies,
|
"cookies": cookies,
|
||||||
"gateway_host": self.headers.get("Host", f"localhost:{GATEWAY_PORT}"),
|
"gateway_host": self.headers.get("Host", f"localhost:{GATEWAY_PORT}"),
|
||||||
|
"scheme": self.headers.get("X-Forwarded-Proto", "http"),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -164,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
|
||||||
|
|
|
||||||
|
|
@ -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_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,
|
||||||
|
|
@ -48,6 +48,7 @@ def _dispatch_inner(data):
|
||||||
query = data.get("query", {})
|
query = data.get("query", {})
|
||||||
body = data.get("body", {})
|
body = data.get("body", {})
|
||||||
gateway_host = data.get("gateway_host", "")
|
gateway_host = data.get("gateway_host", "")
|
||||||
|
scheme = data.get("scheme", "http")
|
||||||
|
|
||||||
def extract_id(prefix):
|
def extract_id(prefix):
|
||||||
try:
|
try:
|
||||||
|
|
@ -59,24 +60,20 @@ def _dispatch_inner(data):
|
||||||
if path == "/":
|
if path == "/":
|
||||||
return handle_search(query)
|
return handle_search(query)
|
||||||
elif path == "/add":
|
elif path == "/add":
|
||||||
action_type = query.get("type", ["index"])[0]
|
|
||||||
prefill_url = query.get("url", [""])[0].strip()
|
prefill_url = query.get("url", [""])[0].strip()
|
||||||
return handle_add_form(
|
return handle_add_form(prefill_url=prefill_url)
|
||||||
action_type=action_type if action_type == "subscribe" else "index",
|
|
||||||
prefill_url=prefill_url,
|
|
||||||
)
|
|
||||||
elif path == "/pages":
|
elif path == "/pages":
|
||||||
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)
|
||||||
elif path == "/bookmark":
|
elif path == "/bookmark":
|
||||||
return handle_bookmark(query)
|
return handle_bookmark(query)
|
||||||
elif path == "/style":
|
elif path == "/style":
|
||||||
return handle_style_form()
|
return handle_style_form(gateway_host=gateway_host, scheme=scheme)
|
||||||
elif path == "/share/preview":
|
elif path == "/share/preview":
|
||||||
return handle_share_preview()
|
return handle_share_preview()
|
||||||
elif path == "/about":
|
elif path == "/about":
|
||||||
|
|
@ -96,6 +93,8 @@ def _dispatch_inner(data):
|
||||||
return handle_api_sites(query)
|
return handle_api_sites(query)
|
||||||
elif path == "/subscriptions":
|
elif path == "/subscriptions":
|
||||||
return handle_subscriptions()
|
return handle_subscriptions()
|
||||||
|
elif path == "/subscriptions/add":
|
||||||
|
return handle_add_form(action_type="subscribe")
|
||||||
elif path.startswith("/subscriptions/browse/"):
|
elif path.startswith("/subscriptions/browse/"):
|
||||||
sid = extract_id("/subscriptions/browse/")
|
sid = extract_id("/subscriptions/browse/")
|
||||||
return handle_subscription_browse(sid) if sid is not None else _error(400)
|
return handle_subscription_browse(sid) if sid is not None else _error(400)
|
||||||
|
|
@ -123,14 +122,20 @@ def _dispatch_inner(data):
|
||||||
pid = extract_id("/delete/")
|
pid = extract_id("/delete/")
|
||||||
return handle_delete(pid) if pid is not None else _error(400)
|
return handle_delete(pid) if pid is not None else _error(400)
|
||||||
elif path == "/style":
|
elif path == "/style":
|
||||||
return handle_style_submit(body)
|
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":
|
elif path == "/style/reset":
|
||||||
set_setting("custom_template", "")
|
set_setting("custom_template", "")
|
||||||
return handle_style_form("Template reset to default.")
|
_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.")
|
_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":
|
||||||
|
|
|
||||||
|
|
@ -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": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,21 @@
|
||||||
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, _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 handle_style_form(msg=""):
|
|
||||||
|
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
|
template = get_setting("custom_template") or DEFAULT_TEMPLATE
|
||||||
name = get_site_name()
|
name = get_site_name()
|
||||||
sharing = get_setting("sharing_enabled", "0")
|
sharing = get_setting("sharing_enabled", "0")
|
||||||
|
|
@ -21,7 +31,6 @@ def handle_style_form(msg=""):
|
||||||
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=""):
|
||||||
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'<nav>'
|
||||||
|
f'<a href="#site-name">site name</a>'
|
||||||
|
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'<form method="post" action="/style">'
|
||||||
f'{_csrf_field()}'
|
f"{csrf}"
|
||||||
f'<input name="site_name" value="{esc(name)}" placeholder="tinyweb" size="30"><br><br>'
|
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=""):
|
||||||
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,93 +163,135 @@ def handle_style_form(msg=""):
|
||||||
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"<h2>custom html</h2>"
|
|
||||||
f"<p>Edit the full page template. Use <code>{esc('{{content}}')}</code> "
|
|
||||||
f"where page content should appear.</p>"
|
|
||||||
f'<textarea name="template" rows="20" cols="60">{esc(template)}</textarea><br><br>'
|
|
||||||
f'<button type="submit">save</button>'
|
|
||||||
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'<p><a href="javascript:void(fetch(\'http://localhost:8080/bookmark?url=\'+encodeURIComponent(location.href)+\'&token={_get_bookmark_token()}\').then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>'
|
f"<section id=\"template\">"
|
||||||
f"<h2>reset</h2>"
|
f"<h2>template</h2>"
|
||||||
f'<form method="post" action="/style/reset" '
|
f"<p>Edit the full page template.</p>"
|
||||||
f'onsubmit="return confirm(\'Reset the template to default? Your custom template will be lost.\')">'
|
f'<form method="post" action="/style/template">'
|
||||||
f'{_csrf_field()}'
|
f"{csrf}"
|
||||||
f'<button type="submit">reset template to default</button>'
|
f'<textarea name="template" rows="12" cols="60" style="width:100%">{esc(template)}</textarea><br><br>'
|
||||||
|
f'<input type="submit" value="save template">'
|
||||||
f"</form>"
|
f"</form>"
|
||||||
f"<h2>maintenance</h2>"
|
f"</section>"
|
||||||
|
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"<h3>reset template</h3>"
|
||||||
|
f'<form method="post" action="/style/reset">'
|
||||||
|
f"{csrf}"
|
||||||
|
f'<input type="submit" value="reset to default">'
|
||||||
|
f"</form>"
|
||||||
|
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):
|
def handle_style_submit(body, gateway_host="", scheme="http"):
|
||||||
|
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")
|
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("custom_template", template if template.strip() != DEFAULT_TEMPLATE.strip() else "")
|
||||||
set_setting("site_name", name or "tinyweb")
|
_set_flash("Template saved.")
|
||||||
set_setting("sharing_enabled", sharing)
|
return _redirect("/style")
|
||||||
set_setting("semantic_search", semantic)
|
|
||||||
set_setting("use_reranker", reranker)
|
|
||||||
set_setting("compress_embeddings", compress)
|
def handle_field_save(body):
|
||||||
set_setting("tcp_enabled", tcp_enabled)
|
key = body.get("key", [""])[0].strip()
|
||||||
if transport_host:
|
value = body.get("value", [""])[0].strip()
|
||||||
set_setting("transport_host", transport_host)
|
if not key:
|
||||||
if transport_port:
|
return _json_response({"status": "error", "message": "No key provided."}, 400)
|
||||||
set_setting("transport_port", transport_port)
|
if key == "forum_enabled":
|
||||||
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
|
from handlers import forum_plugin
|
||||||
if forum_enabled == "1" and forum_plugin is None:
|
if value == "1" and forum_plugin is None:
|
||||||
return handle_style_form(
|
return _json_response({"status": "error", "message": "Forum plugin not installed."}, 400)
|
||||||
"Forum plugin not installed. Run: pip install tinyweb-forum"
|
if value == "1":
|
||||||
)
|
|
||||||
if forum_enabled == "1":
|
|
||||||
forum_plugin.enable()
|
forum_plugin.enable()
|
||||||
try:
|
try:
|
||||||
forum_plugin.fdb.set_setting("forum_enabled", "1")
|
forum_plugin.fdb.set_setting("forum_enabled", "1")
|
||||||
|
|
@ -206,9 +303,9 @@ def handle_style_submit(body):
|
||||||
forum_plugin.fdb.set_setting("forum_enabled", "0")
|
forum_plugin.fdb.set_setting("forum_enabled", "0")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
set_setting("forum_enabled", forum_enabled)
|
templates_mod.FORUM_ENABLED = (value == "1")
|
||||||
templates_mod.FORUM_ENABLED = (forum_enabled == "1")
|
set_setting(key, value)
|
||||||
return handle_style_form("Saved. Restart TinyWeb for mesh network changes to take effect.")
|
return _json_response({"status": "ok", "message": ""})
|
||||||
|
|
||||||
|
|
||||||
def handle_about():
|
def handle_about():
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ def handle_add_submit(body):
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e).lower()
|
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(
|
return _respond(
|
||||||
f"<h1>add url (manual entry)</h1>"
|
f"<h1>add url (manual entry)</h1>"
|
||||||
f"<p><strong>{esc(url)}</strong> blocks automated access. "
|
f"<p><strong>{esc(url)}</strong> blocks automated access. "
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,23 @@ def handle_search(query):
|
||||||
else:
|
else:
|
||||||
fused_ids = bm25_ids
|
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)
|
total_results = len(fused_ids)
|
||||||
page_ids = fused_ids[offset:offset + PER_PAGE]
|
page_ids = fused_ids[offset:offset + PER_PAGE]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}'
|
||||||
|
|
@ -198,10 +202,11 @@ def handle_subscriptions(msg=""):
|
||||||
f'<input name="dest_hash" placeholder="destination hash" size="40"> '
|
f'<input name="dest_hash" placeholder="destination hash" size="40"> '
|
||||||
f'<button>subscribe</button>'
|
f'<button>subscribe</button>'
|
||||||
f'</form>'
|
f'</form>'
|
||||||
f'<p><small>or <a href="/add?type=subscribe">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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
54
templates.py
54
templates.py
|
|
@ -7,52 +7,29 @@ def esc(s):
|
||||||
return html.escape(str(s))
|
return html.escape(str(s))
|
||||||
|
|
||||||
|
|
||||||
FORUM_CSS = """
|
def _nav_html():
|
||||||
<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>"""
|
|
||||||
|
|
||||||
DEFAULT_TEMPLATE = "<html>\n<head>\n<meta name=\"referrer\" content=\"no-referrer\">\n<meta http-equiv=\"x-dns-prefetch-control\" content=\"off\">\n" + FORUM_CSS + "</head>\n<body>\n{{content}}\n</body>\n</html>"
|
|
||||||
|
|
||||||
|
|
||||||
def _default_template():
|
|
||||||
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 ""
|
||||||
return (
|
return (
|
||||||
'<html>\n<head>\n<meta name="referrer" content="no-referrer">\n<meta http-equiv="x-dns-prefetch-control" content="off">\n'
|
|
||||||
f'{FORUM_CSS}</head>\n<body>\n'
|
|
||||||
f'<p><b><a href="/">{name}</a></b>'
|
f'<p><b><a href="/">{name}</a></b>'
|
||||||
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
' | <a href="/">search</a> | <a href="/pages">browse</a>'
|
||||||
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
|
' | <a href="/tags">tags</a> | <a href="/subscriptions">subscriptions</a>'
|
||||||
f'{forum_link}'
|
f'{forum_link}'
|
||||||
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
|
' | <a href="/style">customize</a> | <a href="/about">about</a></p>\n'
|
||||||
"<hr>\n{{content}}\n</body>\n</html>"
|
"<hr>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_TEMPLATE = "<html>\n<head>\n<meta name=\"referrer\" content=\"no-referrer\">\n<meta http-equiv=\"x-dns-prefetch-control\" content=\"off\">\n</head>\n<body>\n{{nav}}{{content}}\n</body>\n</html>"
|
||||||
|
|
||||||
|
|
||||||
|
def _default_template():
|
||||||
|
return (
|
||||||
|
'<html>\n<head>\n<meta name="referrer" content="no-referrer">\n<meta http-equiv="x-dns-prefetch-control" content="off">\n'
|
||||||
|
'</head>\n<body>\n{{nav}}{{content}}\n</body>\n</html>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
|
|
@ -62,8 +39,7 @@ def wrap_page(body_html, use_default=False):
|
||||||
forum_link = ' <a href="/forum">forum</a>' if FORUM_ENABLED else ""
|
forum_link = ' <a href="/forum">forum</a>' if FORUM_ENABLED else ""
|
||||||
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")))
|
||||||
# Inject forum layout CSS into <head> for any template
|
template = template.replace("{{nav}}", _nav_html())
|
||||||
head_end = "</head>"
|
if head_html:
|
||||||
if head_end in template and FORUM_CSS not in template:
|
template = template.replace("</head>", head_html + "</head>")
|
||||||
template = template.replace(head_end, FORUM_CSS + head_end)
|
|
||||||
return template.replace("{{content}}", body_html)
|
return template.replace("{{content}}", body_html)
|
||||||
|
|
|
||||||
|
|
@ -373,12 +373,25 @@
|
||||||
/* inputs */
|
/* inputs */
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="url"],
|
input[type="url"],
|
||||||
|
input[type="search"],
|
||||||
|
input:not([type]),
|
||||||
input[name="q"],
|
input[name="q"],
|
||||||
|
input[name="title"],
|
||||||
input[name="url"],
|
input[name="url"],
|
||||||
input[name="note"],
|
|
||||||
input[name="tags"],
|
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="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);
|
background: rgba(20, 15, 50, 0.6);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
@ -547,6 +560,34 @@
|
||||||
|
|
||||||
hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; }
|
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 {
|
small {
|
||||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
|
@ -615,11 +656,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<nav>
|
<nav>
|
||||||
<a class="site" href="/">tinyweb</a>
|
<a class="site" href="/">{{site_name}}</a>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="/pages">browse</a>
|
<a href="/pages">browse</a>
|
||||||
<a href="/tags">tags</a>
|
<a href="/tags">tags</a>
|
||||||
<a href="/subscriptions">network</a>
|
<a href="/subscriptions">network</a>
|
||||||
|
{{forum_link}}
|
||||||
<a href="/style">customize</a>
|
<a href="/style">customize</a>
|
||||||
<a href="/about">about</a>
|
<a href="/about">about</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
2169
themes/kodama.html
2169
themes/kodama.html
File diff suppressed because it is too large
Load diff
1424
themes/kodama2.html
1424
themes/kodama2.html
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue