revert: remove LifeOS deployment
Evaluated and torn down at the user's request. Container, image, source (/opt/lifeos), SQLite data (/var/lib/lifeos) and compose dir removed from hf-pdocker-02; Prometheus probe removed and both instances reloaded; port 8095 released. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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: () => [],
|
||||
});
|
||||
@@ -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("/<path:path>")
|
||||
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
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Reference in New Issue
Block a user