Spaces:
Sleeping
Habit Journal β Architecture
Single-user PWA on a Hugging Face Docker Space: FastAPI backend + Vite/Preact frontend, JSON/JSONL under DATA_ROOT, server-owned stats. Optional OpenRouter for coach/reschedule with Rules backup.
Interactive canvas (Cursor): habit-journal-architecture.canvas.tsx
Stack at a glance
| Layer | Technology |
|---|---|
| Host | Hugging Face Docker Space |
| Server | Uvicorn + FastAPI (app.main:create_app) |
| Static UI | Vite β static/ (PWA shell + service worker) |
| Data | JSON/JSONL under DATA_ROOT (default /data/loop_logger) |
| Optional LLM | OpenRouter (coach + reschedule); Rules backup if missing |
One line: HF Docker β Uvicorn/FastAPI β JSONL stores β Vite/Preact PWA (credentials: 'include') β optional OpenRouter with Rules backup.
System overview
flowchart TB
subgraph clients [Clients]
Browser[Browser PWA]
Agent[Cursor agent]
end
subgraph space [HF Docker Space]
Static[static/ Vite build]
API[FastAPI create_app]
SW[Service Worker shell-only]
end
subgraph data [DATA_ROOT]
Config[config.json]
Entries[entries.jsonl]
Daily[daily.jsonl]
Traces[traces.jsonl]
Sched[schedule/]
end
OR[OpenRouter optional]
Browser --> Static
Browser --> API
Browser --> SW
Agent -->|Bearer AGENT_TOKEN| API
API --> Config
API --> Entries
API --> Daily
API --> Traces
API --> Sched
API -.->|coach / reschedule| OR
Notes:
- Browser loads the static shell;
/api/*is network-only (SW must never cache API). - Agent uses
Authorization: Bearer AGENT_TOKENon/api/agent/*(session login also works as fallback).
Backend layers
flowchart LR
subgraph edge [HTTP]
RAuth[auth]
REnt[entries]
RDaily[daily]
RPlan[plan]
RAgent[agent]
RCoach[coach]
RStats[stats]
RDbg[debug/export/settings/health]
end
subgraph services [Services]
CoachSvc[CoachService]
Resched[RescheduleService]
Evidence[evidence + stats_math]
SchedMath[schedule_math]
end
subgraph stores [Stores]
EntryStore
DailyStore
ScheduleStore
TraceStore
ConfigStore
end
RAuth --> ConfigStore
REnt --> EntryStore
RDaily --> DailyStore
RPlan --> ScheduleStore
RPlan --> Resched
RAgent --> ScheduleStore
RAgent --> DailyStore
RAgent --> EntryStore
RCoach --> CoachSvc
CoachSvc --> Evidence
CoachSvc --> TraceStore
RStats --> Evidence
Resched --> SchedMath
ScheduleStore --> SchedMath
Module map
| Module | Responsibility |
|---|---|
app/routers/* |
HTTP edge; envelope {ok, data, error} |
app/store_* |
JSONL persistence + file locks |
app/stats_math.py, app/evidence.py |
Server-owned ranks & SERVER_PICKS |
app/coach_service.py, app/backup_replies.py |
OpenRouter or Rules backup |
app/schedule_* |
Plan math, locked EV, reschedule |
app/deps.py, app/security_passwords.py |
Session + AGENT_TOKEN bearer |
API surface
| Group | Endpoints |
|---|---|
| Health | GET /api/health, GET /api/ready |
| Auth | POST login / logout / change-password, GET /api/auth/me |
| Entries | CRUD /api/entries + filters |
| Daily | PUT/GET /api/daily/{day}, GET /api/daily?start=&end= |
| Plan | Plan CRUD, feedback, seed, reschedule, priors, templates, health |
| Agent | GET /api/agent/context, PUT /api/agent/plan/{day} |
| Coach | POST /api/coach, POST /api/entries/{id}/coach |
| Stats | GET /api/stats, GET /api/stats/remedies |
| Debug | Traces, paste bundles |
| Export | Entries/daily JSONL + CSV |
| Settings | GET /api/settings/status (no secrets) |
Frontend UI
Nav IA
Home | Plan | (+) Log | Daily | Settings
History remains at #/history (reachable from Home βSee allβ). Center FAB stays Log (one tap).
flowchart TB
Login["#/login"] --> AuthGate{session?}
AuthGate -->|yes| Shell[App shell]
Shell --> TabBar
subgraph tabs [Bottom nav]
Home["#/ Home"]
Plan["#/plan Plan"]
FAB["(+) Log"]
Daily["#/daily Daily"]
Settings["#/settings"]
end
TabBar --> Home
TabBar --> Plan
TabBar --> FAB
TabBar --> Daily
TabBar --> Settings
Home --> History["#/history"]
History --> Entry["#/entry/:id"]
Home --> Coach["#/coach"]
Home --> Stats["#/stats"]
Settings --> Remedies["#/remedies"]
FAB --> Log["#/log"]
Log -->|high FSE| ProofSheet[Proof brick sheet]
ProofSheet --> DailyAPI[PUT /api/daily]
Plan --> BlockSheet[Block feedback sheet]
Routes
| Route | Page | Role |
|---|---|---|
#/ |
Home | Week score, pending, plan next, indoors/proof nudges |
#/plan, #/plan/:date |
Plan | Day timeline, seed, reschedule, block feedback |
#/log |
Log | Fast entry + high-FSE proof wizard |
#/daily |
Daily | 6-pt scoreboard + movement / fantasy / proof fields |
#/settings |
Settings | Status, password, export |
#/history |
History | Entry list (via Home) |
#/entry/:id |
Entry | Detail + coach |
#/coach |
Coach | Free-text coach + SERVER_PICKS |
#/stats |
Stats | Server-owned p_helped / ranks |
#/remedies |
Remedies | Remedy leaderboard |
#/login |
Login | Password β session cookie |
UI stack
- Vite + Preact + TypeScript
- Motion (press / sheets)
- lucide-preact icons
- Plus Jakarta Sans, warm journal UI (
#EBE7E0) credentials: 'include'on all API calls;401β#/login
Key components
| Component | Role |
|---|---|
TabBar |
Home | Plan | (+) Log | Daily | Settings |
Timeline + BlockSheet |
Plan day + thick feedback |
ResolvePendingSheet |
Pending resolve in < 3 taps |
Toast / Sheet / Pressable |
Shared chrome |
CoachResult |
Rules | Model badge |
Auth
sequenceDiagram
participant U as User
participant UI as PWA
participant API as FastAPI
participant Disk as config.json
U->>UI: password
UI->>API: POST /api/auth/login
API->>Disk: verify PBKDF2 hash
API-->>UI: Set-Cookie session
UI->>API: GET /api/* credentials include
Note over API: 401 β redirect #/login
participant A as Agent
A->>API: GET /api/agent/context
Note over A,API: Authorization Bearer AGENT_TOKEN
- Human: PBKDF2 password in
config.jsonβ signed session cookie (sv+exp). - Agent: Bearer
AGENT_TOKENon/api/agent/*(or session). - Envelope:
{ ok, data, error: { code, message } }. - Server owns points,
p_helped, ranks,d_hatβ LLM must not invent %.
Coach pipeline
flowchart TB
Req[POST /api/coach] --> Evidence[build_evidence SERVER_PICKS]
Evidence --> Key{OPENROUTER_API_KEY?}
Key -->|no / force_backup| Rules[backup_replies]
Key -->|yes| OR[OpenRouter]
OR -->|parse fail / label at high I| Rules
Rules -->|I ge 7| HighFSE[high_intensity_fse]
OR --> Trace[traces.jsonl]
Rules --> Trace
Trace --> UI[CoachResult Rules or Model]
At intensity β₯ 7, Rules prefer interruptive remedies (leave room, interrupt walk without cinematic headphones, one proof line) β not βLabelingβ alone.
Schedule / Plan
flowchart TB
AgentPull[GET agent/context] --> Caps[capacity_hint + operators]
Caps --> Put[PUT agent/plan or UI seed]
Put --> PlanFile[plan-YYYY-MM-DD.json]
UI[Timeline + BlockSheet] --> FB[POST feedback]
FB --> Feedback[feedback.jsonl]
Feedback --> Priors[priors.json d_hat]
UI --> Resched[POST reschedule]
Resched -->|OR or rules| PlanFile
P1 scope lock: thin loop only (timeline + thick feedback + priors + capacity cuts). EV formula locked; no PERMA / Ξ΄ / H in code. See SCHEDULE.md.
Domain: loop-break / movement
flowchart LR
Log[Log entry] -->|intensity ge 7| Proof[Proof wizard]
Proof --> DailyRow[Daily proof_brick_*]
DailyRow --> Home[Home banners]
DailyRow --> AgentCtx[agent/context movement]
Walk[Interrupt vs fantasy walk] --> DailyRow
Coach[Coach Rules] -->|intensity ge 7| Interrupt[Interrupt not Labeling]
PlanTpl[fantasy_slot_25 template] --> Plan[Day plan]
Daily additive fields include: stayed_indoors_all_day, left_room, left_home, walk splits, headphones / cinematic flags, proof_brick_*, fantasy scheduled vs unplanned minutes. 6-pt Daily formula unchanged.
Persistence layout
Under DATA_ROOT (default /data/loop_logger):
| Path | Purpose |
|---|---|
config.json |
Password hash, session version |
entries.jsonl |
Log entries |
daily.jsonl |
Scoreboard + movement / proof / fantasy |
traces.jsonl |
Coach debug traces |
coach_brief.md |
Coach brief |
schedule/plan-YYYY-MM-DD.json |
Versioned day plan |
schedule/feedback.jsonl |
Block thick feedback |
schedule/priors.json |
d_hat / p_done / fun priors |
schedule/meta.json |
Reschedule cooldown |
Path definitions live in app/paths.py.
Related docs
- BACKEND.md β API / storage / coach / ops
- FRONTEND.md β PWA UI contract
- SCHEDULE.md β Plan / agent / P1 scope lock
Local tooling
scripts/pull_handoff_bundle.pyβ pull live Space state into a paste bundle (handoff/gitignored).envβ local secrets only (gitignored):HF_TOKEN,HF_SPACE_URL,APP_PASSWORD,AGENT_TOKEN