Brain Service: - Playwright stealth crawler replacing browserless (og:image, Readability, Reddit JSON API) - AI classification with tag definitions and folder assignment - YouTube video download via yt-dlp - Karakeep migration complete (96 items) - Taxonomy management (folders with icons/colors, tags) - Discovery shuffle, sort options, search (Meilisearch + pgvector) - Item tag/folder editing, card color accents RSS Reader Service: - Custom FastAPI reader replacing Miniflux - Feed management (add/delete/refresh), category support - Full article extraction via Readability - Background content fetching for new entries - Mark all read with confirmation - Infinite scroll, retention cleanup (30/60 day) - 17 feeds migrated from Miniflux iOS App (SwiftUI): - Native iOS 17+ app with @Observable architecture - Cookie-based auth, configurable gateway URL - Dashboard with custom background photo + frosted glass widgets - Full fitness module (today/templates/goals/food library) - AI assistant chat (fitness + brain, raw JSON state management) - 120fps ProMotion support AI Assistants (Gateway): - Unified dispatcher with fitness/brain domain detection - Fitness: natural language food logging, photo analysis, multi-item splitting - Brain: save/append/update/delete notes, search & answer, undo support - Madiha user gets fitness-only (brain disabled) Firefox Extension: - One-click save to Brain from any page - Login with platform credentials - Right-click context menu (save page/link/image) - Notes field for URL saves - Signed and published on AMO Other: - Reader bookmark button routes to Brain (was Karakeep) - Fitness food library with "Add" button + add-to-meal popup - Kindle send file size check (25MB SMTP2GO limit) - Atelier UI as default (useAtelierShell=true) - Mobile upload box in nav drawer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
6.2 KiB
Python
160 lines
6.2 KiB
Python
"""
|
|
Platform Gateway — Database initialization and access.
|
|
"""
|
|
|
|
import sqlite3
|
|
|
|
import bcrypt
|
|
|
|
from config import (
|
|
DB_PATH, TRIPS_URL, FITNESS_URL, INVENTORY_URL,
|
|
MINIFLUX_URL, READER_URL, SHELFMARK_URL, SPOTIZERR_URL, BUDGET_URL, TASKS_URL, BRAIN_URL,
|
|
)
|
|
|
|
|
|
def get_db():
|
|
conn = sqlite3.connect(str(DB_PATH))
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
conn.execute("PRAGMA journal_mode = WAL")
|
|
return conn
|
|
|
|
|
|
def init_db():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
c.execute('''CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
display_name TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
)''')
|
|
|
|
c.execute('''CREATE TABLE IF NOT EXISTS sessions (
|
|
token TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
expires_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)''')
|
|
|
|
c.execute('''CREATE TABLE IF NOT EXISTS service_connections (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
service TEXT NOT NULL,
|
|
auth_type TEXT NOT NULL DEFAULT 'bearer',
|
|
auth_token TEXT NOT NULL,
|
|
metadata TEXT DEFAULT '{}',
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, service),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)''')
|
|
|
|
c.execute('''CREATE TABLE IF NOT EXISTS apps (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
icon TEXT DEFAULT '',
|
|
route_prefix TEXT NOT NULL,
|
|
proxy_target TEXT NOT NULL,
|
|
sort_order INTEGER DEFAULT 0,
|
|
enabled INTEGER DEFAULT 1,
|
|
dashboard_widget TEXT DEFAULT NULL
|
|
)''')
|
|
|
|
c.execute('''CREATE TABLE IF NOT EXISTS pinned_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
service TEXT NOT NULL DEFAULT 'inventory',
|
|
item_id TEXT NOT NULL,
|
|
item_name TEXT NOT NULL DEFAULT '',
|
|
pinned_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, service, item_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)''')
|
|
|
|
conn.commit()
|
|
|
|
# Seed default apps
|
|
existing = c.execute("SELECT COUNT(*) FROM apps").fetchone()[0]
|
|
if existing == 0:
|
|
c.execute("INSERT INTO apps VALUES ('trips', 'Trips', 'map', '/trips', ?, 1, 1, 'upcoming_trips')", (TRIPS_URL,))
|
|
c.execute("INSERT INTO apps VALUES ('fitness', 'Fitness', 'bar-chart', '/fitness', ?, 2, 1, 'daily_calories')", (FITNESS_URL,))
|
|
c.execute("INSERT INTO apps VALUES ('inventory', 'Inventory', 'package', '/inventory', ?, 3, 1, 'items_issues')", (INVENTORY_URL,))
|
|
conn.commit()
|
|
else:
|
|
# Ensure inventory app exists (migration for existing DBs)
|
|
inv = c.execute("SELECT id FROM apps WHERE id = 'inventory'").fetchone()
|
|
if not inv:
|
|
c.execute("INSERT INTO apps VALUES ('inventory', 'Inventory', 'package', '/inventory', ?, 3, 1, 'items_issues')", (INVENTORY_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added inventory app")
|
|
|
|
# Ensure reader app exists
|
|
rdr = c.execute("SELECT id FROM apps WHERE id = 'reader'").fetchone()
|
|
if not rdr:
|
|
c.execute("INSERT INTO apps VALUES ('reader', 'Reader', 'rss', '/reader', ?, 4, 1, 'unread_count')", (READER_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added reader app")
|
|
else:
|
|
# Update existing reader app to point to new service
|
|
c.execute("UPDATE apps SET proxy_target = ? WHERE id = 'reader'", (READER_URL,))
|
|
conn.commit()
|
|
|
|
# Ensure books app exists (now media)
|
|
books = c.execute("SELECT id FROM apps WHERE id = 'books'").fetchone()
|
|
if not books:
|
|
c.execute("INSERT INTO apps VALUES ('books', 'Media', 'book', '/media', ?, 5, 1, NULL)", (SHELFMARK_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added media app")
|
|
else:
|
|
c.execute("UPDATE apps SET name = 'Media', route_prefix = '/media' WHERE id = 'books'")
|
|
conn.commit()
|
|
|
|
# Ensure music (spotizerr) app exists
|
|
music = c.execute("SELECT id FROM apps WHERE id = 'music'").fetchone()
|
|
if not music:
|
|
c.execute("INSERT INTO apps VALUES ('music', 'Music', 'music', '/music', ?, 6, 1, NULL)", (SPOTIZERR_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added music app")
|
|
|
|
# Ensure budget app exists
|
|
budget = c.execute("SELECT id FROM apps WHERE id = 'budget'").fetchone()
|
|
if not budget:
|
|
c.execute("INSERT OR IGNORE INTO apps VALUES ('budget', 'Budget', 'dollar-sign', '/budget', ?, 7, 1, 'budget_summary')", (BUDGET_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added budget app")
|
|
|
|
# Ensure tasks app exists
|
|
tasks = c.execute("SELECT id FROM apps WHERE id = 'tasks'").fetchone()
|
|
if not tasks:
|
|
c.execute("INSERT INTO apps VALUES ('tasks', 'Tasks', 'check-square', '/tasks', ?, 8, 1, 'today_tasks')", (TASKS_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added tasks app")
|
|
|
|
# Ensure brain app exists
|
|
brain = c.execute("SELECT id FROM apps WHERE id = 'brain'").fetchone()
|
|
if not brain:
|
|
c.execute("INSERT INTO apps VALUES ('brain', 'Brain', 'brain', '/brain', ?, 9, 1, NULL)", (BRAIN_URL,))
|
|
conn.commit()
|
|
print("[Gateway] Added brain app")
|
|
|
|
# Seed admin user from env vars if no users exist
|
|
import os
|
|
user_count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
|
if user_count == 0:
|
|
admin_user = os.environ.get("ADMIN_USERNAME")
|
|
admin_pass = os.environ.get("ADMIN_PASSWORD")
|
|
admin_name = os.environ.get("ADMIN_DISPLAY_NAME", "Admin")
|
|
if not admin_user or not admin_pass:
|
|
print("[Gateway] WARNING: No users exist and ADMIN_USERNAME/ADMIN_PASSWORD not set. Create a user manually.")
|
|
else:
|
|
pw_hash = bcrypt.hashpw(admin_pass.encode(), bcrypt.gensalt()).decode()
|
|
c.execute("INSERT INTO users (username, password_hash, display_name) VALUES (?, ?, ?)",
|
|
(admin_user, pw_hash, admin_name))
|
|
conn.commit()
|
|
print(f"[Gateway] Created admin user: {admin_user}")
|
|
|
|
conn.close()
|