Make ThoughtSync installable ("Add to Home Screen") without going
offline-first (the Android app is the real offline client, M5):
- web app manifest (name, icons incl. maskable + SVG, standalone, theme)
- generated PNG icon set + apple-touch-icon + favicon, from committed
SVG sources (a linked-thoughts constellation on the brand tile)
- minimal service worker: installable shell only — caches just an
offline fallback page, never the app shell / hashed assets / API, so
data stays fresh and deploys never serve a stale shell
- register the SW in main.ts (progressive enhancement; failures ignored)
- index.html: manifest/icon links, apple-mobile meta, description
- backend: register the .webmanifest MIME type so it serves as
application/manifest+json
- README: note that install needs a secure context (HTTPS/localhost)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
import mimetypes
|
|
import os
|
|
import secrets
|
|
from datetime import timedelta
|
|
|
|
from quart import Quart, jsonify, send_from_directory
|
|
|
|
from . import __version__
|
|
from .auth import bp as auth_bp
|
|
from .config import Config
|
|
from .db import session_scope
|
|
from .graph import bp as graph_bp
|
|
from .labels import bp as labels_bp
|
|
from .notes import bp as notes_bp
|
|
from .settings import get_public_config, get_setting, load_or_create_secret_key
|
|
from .settings_api import bp as settings_bp
|
|
|
|
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
|
|
|
# `.webmanifest` isn't in every base image's mime map; register it so the PWA
|
|
# manifest is served as application/manifest+json instead of octet-stream.
|
|
mimetypes.add_type("application/manifest+json", ".webmanifest")
|
|
|
|
|
|
def create_app() -> Quart:
|
|
# static_folder=None: the SPA catch-all below owns static serving.
|
|
app = Quart(__name__, static_folder=None)
|
|
# Ephemeral/env secret so the app (and DB-free unit tests) construct without a
|
|
# database. before_serving swaps in the real, DB-persisted key before serving.
|
|
app.secret_key = Config.secret_key_env() or secrets.token_urlsafe(48)
|
|
app.config["APP_VERSION"] = os.environ.get("APP_VERSION", __version__)
|
|
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
|
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
|
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30)
|
|
app.config["MAX_CONTENT_LENGTH"] = 12 * 1024 * 1024 # image-upload body cap
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(notes_bp)
|
|
app.register_blueprint(labels_bp)
|
|
app.register_blueprint(graph_bp)
|
|
app.register_blueprint(settings_bp)
|
|
|
|
@app.before_serving
|
|
async def _bootstrap() -> None:
|
|
# Load (or generate + persist) the real signing secret and the live session
|
|
# lifetime from the DB, before any request is served.
|
|
async with session_scope() as db:
|
|
app.secret_key = await load_or_create_secret_key(db)
|
|
try:
|
|
days = int(await get_setting(db, "session_ttl_days"))
|
|
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=days)
|
|
except (ValueError, TypeError, KeyError):
|
|
pass
|
|
|
|
@app.get("/api/health")
|
|
async def health():
|
|
return jsonify({"status": "ok", "version": app.config["APP_VERSION"]})
|
|
|
|
@app.get("/api/config")
|
|
async def public_config():
|
|
# Public: the login/register screen reads site name + whether signups are open.
|
|
async with session_scope() as db:
|
|
data = await get_public_config(db)
|
|
data["version"] = app.config["APP_VERSION"]
|
|
return jsonify(data)
|
|
|
|
@app.get("/", defaults={"path": ""})
|
|
@app.get("/<path:path>")
|
|
async def spa(path: str):
|
|
if path.startswith("api/"):
|
|
return jsonify({"error": "not found"}), 404
|
|
candidate = os.path.join(STATIC_DIR, path)
|
|
if path and os.path.isfile(candidate):
|
|
return await send_from_directory(STATIC_DIR, path)
|
|
index = os.path.join(STATIC_DIR, "index.html")
|
|
if os.path.isfile(index):
|
|
return await send_from_directory(STATIC_DIR, "index.html")
|
|
return jsonify({"error": "frontend not built"}), 404
|
|
|
|
return app
|