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/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:
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 "$@"
diff --git a/gateway.py b/gateway.py
index 7d3fd53..fa4b076 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:
@@ -164,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 95d1753..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_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,
@@ -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:
@@ -59,24 +60,20 @@ 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/"):
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)
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":
@@ -96,6 +93,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)
@@ -123,14 +122,20 @@ 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/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.")
+ _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.")
+ _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 1f2568b..6b3786d 100644
--- a/handlers/customize.py
+++ b/handlers/customize.py
@@ -1,11 +1,21 @@
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, _redirect, _json_response, _csrf_field, _get_bookmark_token, _request_local
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
name = get_site_name()
sharing = get_setting("sharing_enabled", "0")
@@ -21,7 +31,6 @@ def handle_style_form(msg=""):
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=""):
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""
)
+ 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"{msg_html}"
+ f''
+ f'site name '
+ f' · sharing '
+ f'{forum_nav}'
+ f' · search '
+ f' · mesh '
+ f' · template '
+ f' · tools '
+ f' '
+ f" "
+ f""
+ f" "
+ f""
+ f" "
+ f"{forum_body}"
+ f" "
+ f""
+ f" "
+ f""
+ 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"reset "
- f''
- f'{_csrf_field()}'
- f'reset template to default '
+ f" "
+ f" "
+ f""
+ f" "
+ f""
f'back ',
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")
- 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:
+ _set_flash("Template saved.")
+ return _redirect("/style")
+
+
+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 forum_enabled == "1" and forum_plugin is None:
- return handle_style_form(
- "Forum plugin not installed. Run: pip install tinyweb-forum"
- )
- if forum_enabled == "1":
+ 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")
@@ -206,9 +303,9 @@ def handle_style_submit(body):
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.")
+ templates_mod.FORUM_ENABLED = (value == "1")
+ set_setting(key, value)
+ return _json_response({"status": "ok", "message": ""})
def handle_about():
diff --git a/handlers/pages.py b/handlers/pages.py
index 3e213e0..0799251 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. "
@@ -172,7 +172,7 @@ def handle_pages(query=None):
f'
'
f'{esc(r["title"])} {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'Title: '
f' '
f'Summary (shown in search results): '
@@ -310,7 +313,7 @@ def handle_edit_form(page_id, msg=""):
f'save '
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/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]
diff --git a/handlers/subscriptions.py b/handlers/subscriptions.py
index b54a253..97f20b2 100644
--- a/handlers/subscriptions.py
+++ b/handlers/subscriptions.py
@@ -162,7 +162,7 @@ def handle_subscriptions(msg=""):
sync_btn = 'syncing... '
else:
sync_btn = (
- f''
+ f' '
f'{_csrf_field()}sync now '
)
@@ -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()}auto-sync: {auto_label} '
- f'
'
+ f' '
f'{_csrf_field()}remove '
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 = 'syncing... ' if any_syncing else 'sync all '
listing = (
f'{cards}'
@@ -198,10 +202,11 @@ def handle_subscriptions(msg=""):
f' '
f'subscribe '
f''
- f'or subscribe to an instance
'
+ 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 777ed63..27a1a6b 100644
--- a/templates.py
+++ b/templates.py
@@ -7,52 +7,29 @@ def esc(s):
return html.escape(str(s))
-FORUM_CSS = """
-"""
-
-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\n\n{{nav}}{{content}}\n\n"
+
+
+def _default_template():
+ return (
+ '\n\n \n \n'
+ '\n\n{{nav}}{{content}}\n\n'
)
-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:
@@ -62,8 +39,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")))
- # 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)
+ template = template.replace("{{nav}}", _nav_html())
+ 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/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 @@
- tinyweb
+ {{site_name}}
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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- tinyweb
-
-
-
-
- {{content}}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file