Single-command startup and fix bookmarklet

app.py now auto-starts the gateway HTTP server in a daemon thread,
so users only need `python app.py` to get everything running. The
gateway calls dispatch_request directly when co-located (local mode)
instead of trying to establish an RNS link to itself. Bookmarklet
hardcoded to localhost:8080. gateway.py still works standalone for
connecting to remote instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Derick Phan 2026-03-25 23:01:54 -07:00
parent 9a9b5e0617
commit 4e4cc69e0f
No known key found for this signature in database
3 changed files with 52 additions and 34 deletions

18
app.py
View file

@ -1,9 +1,12 @@
import os
import time
import threading
import RNS
from http.server import HTTPServer
from db import init_db
from handlers import dispatch_request
from gateway import GatewayState, GatewayHandler, GATEWAY_PORT
APP_NAME = "tinyweb"
ASPECTS = ["server"]
@ -24,6 +27,14 @@ def rns_request_handler(path, data, request_id, link_id, remote_identity, reques
return dispatch_request(data)
def start_gateway(reticulum):
GatewayState.reticulum = reticulum
GatewayState.local_dispatch = dispatch_request
server = HTTPServer(("127.0.0.1", GATEWAY_PORT), GatewayHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
def main():
init_db()
reticulum = RNS.Reticulum()
@ -44,10 +55,11 @@ def main():
)
destination.announce()
start_gateway(reticulum)
print(f"TinyWeb Reticulum server running")
print(f"Destination hash: {RNS.prettyhexrep(destination.hash)}")
print(f"Share this hash with clients to connect via gateway.py")
print(f"TinyWeb running!")
print(f"Open http://localhost:{GATEWAY_PORT} in your browser")
print(f"Destination hash: {RNS.prettyhexrep(destination.hash)} (share this so friends can subscribe)")
while True:
time.sleep(1)

View file

@ -16,6 +16,7 @@ class GatewayState:
destination = None
link = None
link_lock = threading.Lock()
local_dispatch = None # set when running inside app.py
def resolve_destination(dest_hash_hex):
@ -83,34 +84,40 @@ class GatewayHandler(BaseHTTPRequestHandler):
}
try:
link = ensure_link()
receipt = link.request(
"/tinyweb",
data=request_data,
timeout=REQUEST_TIMEOUT,
)
# Wait for the response
elapsed = 0
done_statuses = (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED, RNS.RequestReceipt.FAILED)
while receipt.get_status() not in done_statuses and elapsed < REQUEST_TIMEOUT:
time.sleep(0.1)
elapsed += 0.1
if receipt.get_status() in (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED):
resp = receipt.get_response()
self.send_response(resp["status"])
self.send_header("Content-Type", resp.get("content_type", "text/html; charset=utf-8"))
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)
elif receipt.get_status() == RNS.RequestReceipt.FAILED:
self.send_error(504, "Request to TinyWeb server failed")
if GatewayState.local_dispatch:
resp = GatewayState.local_dispatch(request_data)
else:
self.send_error(504, "Request to TinyWeb server timed out")
link = ensure_link()
receipt = link.request(
"/tinyweb",
data=request_data,
timeout=REQUEST_TIMEOUT,
)
# Wait for the response
elapsed = 0
done_statuses = (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED, RNS.RequestReceipt.FAILED)
while receipt.get_status() not in done_statuses and elapsed < REQUEST_TIMEOUT:
time.sleep(0.1)
elapsed += 0.1
if receipt.get_status() in (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED):
resp = receipt.get_response()
elif receipt.get_status() == RNS.RequestReceipt.FAILED:
self.send_error(504, "Request to TinyWeb server failed")
return
else:
self.send_error(504, "Request to TinyWeb server timed out")
return
self.send_response(resp["status"])
self.send_header("Content-Type", resp.get("content_type", "text/html; charset=utf-8"))
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)
except ConnectionError as e:
GatewayState.link = None

View file

@ -308,12 +308,11 @@ def handle_import_submit(body):
return handle_import_form(f"Imported {imported} page(s). {errors} error(s).")
def handle_style_form(msg="", gateway_host=""):
def handle_style_form(msg=""):
css = get_setting("custom_css")
name = get_site_name()
sharing = get_setting("sharing_enabled", "0")
checked = " checked" if sharing == "1" else ""
host = gateway_host or "localhost:8080"
return _respond(
f"<h1>customize</h1>"
f"<h2>name your search engine</h2>"
@ -340,7 +339,7 @@ def handle_style_form(msg="", gateway_host=""):
f"</form>"
f"<h2>bookmarklet</h2>"
f"<p>Drag this link to your bookmarks bar. Click it on any page to index it instantly.</p>"
f'<p><a href="javascript:void(fetch(\'http://{esc(host)}/bookmark?url=\'+encodeURIComponent(location.href)).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(\'http://localhost:8080/bookmark?url=\'+encodeURIComponent(location.href)).then(r=>r.text()).then(t=>alert(t)).catch(()=>alert(\'tinyweb not running\')))">+ save to {esc(name)}</a></p>'
f"<p>{msg}</p>"
f'<a href="/">back</a>'
)
@ -651,7 +650,7 @@ def dispatch_request(data):
elif path == "/bookmark":
return handle_bookmark(query)
elif path == "/style":
return handle_style_form(gateway_host=gateway_host)
return handle_style_form()
elif path == "/export":
return handle_export()
elif path == "/import":