diff --git a/CLAUDE.md b/CLAUDE.md index 3aa9c4a..ef78d4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,7 +59,6 @@ hf-pdocker-02 (7.6GB RAM, i7-6700T, HDD) ├── [COMPOSE] Wiki.js + wikijs-db (PostgreSQL) ├── [COMPOSE] Vaultwarden, Actual Budget, MCPO, Trivy ├── [COMPOSE] n8n + n8n-db (PostgreSQL) -- workflow automation -├── [COMPOSE] LifeOS (:8095) -- personal life/goals/habits tracker (PWA). Local mode: Flask+SQLite, no Supabase. LAN-only (no auth in backend). AI companion -> Ollama on .150. ├── [COMPOSE] Farm Agent (:8889) ├── [COMPOSE] Node-exporter (:9100) -- standalone with pid:host ├── [COMPOSE] Telegraf -- metrics collection to InfluxDB @@ -183,7 +182,6 @@ compose/ # Standalone Compose definitions (deployed per-node) - Actual Budget: :5006 - MCPO: :8001 - Trivy: :4954 -- LifeOS: :8095 (host) -> :8080 (container). LAN-only, deliberately NOT behind Traefik: the Flask backend is single-user with NO auth. Image `lifeos:local` built on-node from /opt/lifeos (upstream https://github.com/lifeos-app/lifeos, pinned 3009bf6). SQLite at /var/lib/lifeos/data.db on LOCAL disk (NFS breaks SQLite WAL locking). Compose: compose/hf-pdocker-02/lifeos/. Note: host 8080 is taken by unifi-os-server. **bart (192.168.86.167):** - Farm Agent: :8889 diff --git a/compose/hf-pdocker-02/lifeos/Dockerfile b/compose/hf-pdocker-02/lifeos/Dockerfile deleted file mode 100644 index e67f5e1..0000000 --- a/compose/hf-pdocker-02/lifeos/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# LifeOS — https://github.com/lifeos-app/lifeos -# -# Two stages: Node builds the Vite/React bundle, Python serves that bundle plus -# the Flask API from a single process. Build context is the upstream source tree -# (cloned to /opt/lifeos on the node); this file is copied in alongside it. - -# ─── Stage 1: build the frontend ────────────────────────────────────── -FROM node:22-bookworm-slim AS frontend -WORKDIR /build - -COPY package.json package-lock.json ./ -# --ignore-scripts skips the electron-builder postinstall, which pulls a desktop -# toolchain we never use and fails without an X stack. -RUN npm ci --ignore-scripts - -COPY . . - -# Upstream is hosted under an /app base path and hardcodes it in two places. -# We serve at the root, so both must be rewritten or the app renders a blank page: -# - App.tsx pins React Router to basename "/app"; at "/" it matches no route and -# renders NOTHING (silent -- only a console warning, no error). -# - index.html points the PWA manifest and touch icons at /app/... -RUN sed -i 's|basename: "/app"|basename: "/"|' src/App.tsx \ - && sed -i 's|"/app/|"/|g' index.html \ - && grep -q 'basename: "/"' src/App.tsx \ - && ! grep -q '"/app/' index.html - -# initAuth() only boots the single local user for Tauri/Electron, so a browser -# build in local-api mode falls through to a Supabase login screen instead -- -# one the backend cannot serve (it has no /api/auth/login or /api/auth/signup -# routes, being single-user by design). Treat local-api like the desktop envs. -RUN sed -i "s@if (detectedEnv === 'tauri' || detectedEnv === 'electron') {@if (detectedEnv === 'tauri' || detectedEnv === 'electron' || detectedEnv === 'local-api') {@" src/stores/useUserStore.ts \ - && grep -q "detectedEnv === 'local-api') {" src/stores/useUserStore.ts - -# The local API adapter has no realtime layer, so supabase.channel() is undefined -# and every subscribing component crashes the app. Append no-op stubs. -COPY local-api-realtime-shim.ts /tmp/shim.ts -RUN cat /tmp/shim.ts >> src/lib/local-api.ts && rm /tmp/shim.ts - -# Routes the data layer (src/lib/data-access.ts) to the local Flask API instead -# of Supabase Cloud. VITE_API_BASE_URL is deliberately left unset: local-api.ts -# then falls back to a relative /api, so the bundle works on any host and port. -ENV VITE_USE_LOCAL_API=true - -# Dummy Supabase values. Nothing reads Supabase in local mode, but src/lib/supabase.ts -# calls createClient() at module scope, and createClient throws "supabaseUrl is -# required" on an empty string -- which kills the bundle before React mounts, leaving -# a blank page. Its own comment claims it only warns; it does not. A syntactically -# valid dummy URL constructs fine and is never called. -ENV VITE_SUPABASE_URL=https://noop.invalid -ENV VITE_SUPABASE_ANON_KEY=noop - -ENV NODE_OPTIONS=--max-old-space-size=3072 -RUN npm run build - -# ─── Stage 2: Flask API + built frontend ────────────────────────────── -FROM python:3.12-slim -WORKDIR /app - -COPY backend/requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt gunicorn - -COPY backend/ /app/ -COPY serve_container.py /app/ -COPY --from=frontend /build/dist /app/dist - -# Upstream hardcodes the author's own model name in the chat proxy, and a 30s -# timeout that a cold Ollama model load (~46s for a 9.6GB model) blows through. -# Make both env vars. Fails the build if upstream renames these out from under us. -RUN sed -i 's/"model": "sentient-teddy"/"model": os.environ.get("AI_MODEL", "sentient-teddy")/' app.py \ - && sed -i 's/timeout=30$/timeout=int(os.environ.get("AI_TIMEOUT", "120"))/' app.py \ - && grep -q 'os.environ.get("AI_MODEL"' app.py \ - && grep -q 'os.environ.get("AI_TIMEOUT"' app.py - -# app.py hardcodes its SQLite path to ~/.lifeos/data.db, so HOME is what pins -# the database into the mounted volume. -ENV HOME=/data -RUN mkdir -p /data/.lifeos - -EXPOSE 8080 -CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", \ - "--timeout", "60", "--access-logfile", "-", "serve_container:app"] diff --git a/compose/hf-pdocker-02/lifeos/docker-compose.yml b/compose/hf-pdocker-02/lifeos/docker-compose.yml deleted file mode 100644 index f4f31e0..0000000 --- a/compose/hf-pdocker-02/lifeos/docker-compose.yml +++ /dev/null @@ -1,32 +0,0 @@ -services: - lifeos: - build: - context: /opt/lifeos - dockerfile: Dockerfile - image: lifeos:local - container_name: lifeos - restart: unless-stopped - ports: - # 8080 on this node is taken by unifi-os-server. - - "8095:8080" - environment: - - TZ=America/Chicago - # AI companion -> Ollama on the GPU machine (OpenAI-compatible endpoint). - - AI_BRIDGE_URL=http://192.168.86.150:11434 - - AI_MODEL=gemma4:latest - # Ollama needs ~46s to cold-load gemma4 (0.7s once warm); upstream's - # hardcoded 30s timeout would fail every first message. - - AI_TIMEOUT=120 - volumes: - # SQLite (WAL) stays on local disk: NFS v3 locking breaks it, same reason - # guacamole-db lives on /var/lib. - - /var/lib/lifeos:/data/.lifeos - labels: - - "com.centurylinklabs.watchtower.enable=false" - mem_limit: 1g - healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/health-check').read()"] - interval: 60s - timeout: 10s - retries: 3 - start_period: 30s diff --git a/compose/hf-pdocker-02/lifeos/local-api-realtime-shim.ts b/compose/hf-pdocker-02/lifeos/local-api-realtime-shim.ts deleted file mode 100644 index b9141dc..0000000 --- a/compose/hf-pdocker-02/lifeos/local-api-realtime-shim.ts +++ /dev/null @@ -1,30 +0,0 @@ - -// ─── Realtime no-op shim (appended to local-api.ts at image build) ─── -// -// The local Flask API has no websocket/realtime layer -- upstream's LOCAL_MODE.md -// says so outright ("Realtime subscriptions -- NOT migrated"), but the adapter it -// ships still lacks `.channel()`, so every component that opens a subscription -// (useSubscription, useCurrentEvent, social, realm multiplayer) throws -// "TypeError: supabase.channel is not a function" and trips the ErrorBoundary -- -// the whole app renders "Something went wrong". -// -// These stubs satisfy the call shape and do nothing. Reads still work: the app -// fetches through the normal query path. What is lost is live push updates, which -// are meaningless on a single-user instance with no other clients to sync from. -Object.assign(supabase as any, { - channel: (_name?: string, _opts?: any) => { - const ch: any = { - on: () => ch, // chainable, never fires - subscribe: (cb?: (status: string) => void) => { cb?.('SUBSCRIBED'); return ch; }, - unsubscribe: async () => 'ok', - presenceState: () => ({}), // realm multiplayer: nobody else here - track: async () => 'ok', - untrack: async () => 'ok', - send: async () => 'ok', - }; - return ch; - }, - removeChannel: async () => 'ok', - removeAllChannels: async () => 'ok', - getChannels: () => [], -}); diff --git a/compose/hf-pdocker-02/lifeos/serve_container.py b/compose/hf-pdocker-02/lifeos/serve_container.py deleted file mode 100644 index af1ef34..0000000 --- a/compose/hf-pdocker-02/lifeos/serve_container.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Container entrypoint: serve the Flask API and the built SPA from one process. - -Upstream's backend/serve.py does this too, but it also shells out to `npm run -build` and tries to launch a browser, neither of which works in a container. -""" - -from pathlib import Path - -from flask import send_file, send_from_directory - -from app import app # importing app runs init_db(), creating the SQLite schema - -DIST = Path(__file__).parent / "dist" - - -@app.route("/") -def _index(): - return send_file(DIST / "index.html") - - -@app.route("/") -def _static(path): - asset = DIST / path - if asset.is_file(): - return send_from_directory(DIST, path) - if path.startswith("api/"): - return {"error": "Not found"}, 404 - return send_file(DIST / "index.html") # SPA client-side routing diff --git a/lifeos-habits.png b/lifeos-habits.png new file mode 100644 index 0000000..9e3baa5 Binary files /dev/null and b/lifeos-habits.png differ