Spaces:
Sleeping
Backend Design Document
Habit Journal (Discrete PWA) — Hugging Face Spaces
Version: 2.0.0
Scope: Backend API, storage, auth, server-side statistics, coach, traces,
debugging, export, and operations. The frontend is separate.
This is the repository specification. If implementation and this document conflict, this document wins until a human changes it.
Implementation rules
- Work in the vertical slices listed below and keep each diff small.
- Prefer stdlib, FastAPI, httpx, Pydantic, and pydantic-settings.
- Use no SQL database, Redis, Celery, DI framework, or repository hierarchy.
- Every module has a short purpose docstring and public functions are typed.
- Request and response bodies use Pydantic models.
- Secrets come only from environment variables. Never log passwords or keys.
- Keep generic Habit Journal naming and private biography out of git.
- Disable OpenAPI, Swagger, and ReDoc when
ENV=prod. - After each slice, the app must import and start with uvicorn.
- Target at most about 15 Python files under
app/; merge stores if needed.
Runtime contract
On a fresh Hugging Face Docker Space with APP_PASSWORD and
APP_SECRET_KEY, startup creates /data/loop_logger, login works, entries and
daily rows survive restart, statistics are computed in Python, coach always
returns model or backup text, and every debug paste bundle is understandable
without prior user context.
The Docker image uses Python 3.11 slim and runs:
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
Required and optional environment:
APP_PASSWORD: bootstrap password whenconfig.jsondoes not existAPP_SECRET_KEY: required session-signing secretDATA_ROOT=/data/loop_loggerENV=prod;APP_NAME=Habit JournalOPENROUTER_API_KEY;OPENROUTER_MODEL=openrouter/freeOPENROUTER_BASE_URL=https://openrouter.ai/api/v1COACH_TIMEOUT_SEC=25;COACH_TEMPERATURE=0.3COACH_MAX_TOKENS=400;COACH_HISTORY_K=10COACH_TRACE_LIMIT=200MIN_STATS_N=5;STATS_SHRINK_K=3;MATCH_ALPHA=0.5SESSION_MAX_AGE_SEC=604800DEBUG_INCLUDE_FULL_BRIEF=falseLOGIN_RATE_LIMIT=10;LOGIN_RATE_WINDOW_SEC=900
Flat architecture and durable files
Routers call services, services call stores, and stores access the filesystem.
Do not introduce circular imports. main.py owns wiring.
DATA_ROOT contains config.json, entries.jsonl, daily.jsonl,
coach_brief.md, and traces.jsonl. Each mutated path uses a lock. Entry
creation appends one flushed JSONL line under lock. Updates and deletes read,
map/filter, then write a flushed temporary file and os.replace it.
If configuration is absent and APP_PASSWORD exists, create:
{
"password_hash": "pbkdf2_sha256$260000$...",
"session_version": 1,
"created_at": "ISO-8601",
"updated_at": "ISO-8601"
}
PBKDF2 uses SHA-256, at least 200,000 iterations, a random salt, and
hmac.compare_digest. Without config and bootstrap password, readiness is
false and protected data routes return setup_required.
coach_brief.md is operator-editable, read to at most 32 KiB, and seeded with
generic policy only. Debug includes its hash and first 500 characters unless
DEBUG_INCLUDE_FULL_BRIEF=true.
API envelope and security
Every success is {"ok":true,"data":...,"error":null}. Every failure is
{"ok":false,"data":null,"error":{"code":"...","message":"..."}}.
Codes are unauthorized, forbidden, validation_error, not_found,
setup_required, rate_limited, and internal.
Use Starlette signed cookie sessions containing auth, sv, and Unix exp.
Reject expired sessions and sessions whose version differs from config.
Cookies are HTTP-only, SameSite Lax, and secure in production. Rate-limit
login attempts by IP in memory and return generic credential failures.
Limit request bodies to 64 KiB. Do not mount DATA_ROOT. Best-effort strip
emails and long digit runs from coach prompts. Never return stack traces.
Unauthenticated routes are only GET /api/health, GET /api/ready,
POST /api/auth/login, and GET /api/auth/me.
Auth routes:
POST /api/auth/loginwith{password}POST /api/auth/logoutGET /api/auth/mePOST /api/auth/change-passwordwith old/new password; bump session version
Entries and daily rows
Entry fields are id, ts, timestamps, activity, happened, emotions,
intensity, remedy, result, tags, notes, and nested coach metadata.
Activity is 1–500 characters, happened 1–4000, emotions at most 12,
intensity 1–10, remedy and notes at most 2000, and tags at most 20. Normalize
emotions to lowercase stripped text; normalize tags likewise and replace
spaces with underscores. Results are worked, partial, failed, or
pending; pending never enters probability denominators.
Entries support create, list/filter, get, patch, delete, and per-entry coach.
List filters are start/end date, tag, result, emotion, substring q, limit
(50 default, 200 max), and offset; sort newest first.
Daily rows are unique by date and contain brick, brick completion, corn
sessions, delay flag, daydream, rerun, court, points, note, and update time.
Enums are brick A|B|C|D|E|S|none, daydream none|done|fc, rerun clean|R,
and court closed|court. Corn sessions range from 0 to 50.
Server-authoritative daily points:
2 if brick_done
+1 if corn_sessions == 0 or (corn_sessions <= 1 and delay_ok)
+1 if daydream != "fc"
+1 if rerun == "clean"
+1 if court == "closed"
For a seven-calendar-day window, missing days produce incomplete; otherwise
28–42 is strong, 18–27 mixed, and 0–17 escape_heavy.
Server statistics and evidence
stats_math.py contains pure functions only. Excluding pending outcomes:
p_worked(r) = N(worked,r) / N(r)p_helped(r) = N(worked|partial,r) / N(r)p_failed(t) = N(failed,t) / N(t)rank(r) = p_helped(r) * n/(n+k)match(r,T) = |T intersect helped_tags(r)| / max(|T|,1)pick(r) = rank(r) * (1 + alpha*match(r,T))
Intensity buckets are 1–3, 4–6, and 7–10. Emotions are multi-label.
Top coach tables hide remedies below min_n. Server picks are the top five by
pick score. DATA_THIN means fewer than ten scored entries.
Daily statistics include brick done, corn okay, no FC, clean rerun, closed
court, and average points rates. /api/stats returns counts, outcomes,
breakdowns, daily rates, formula strings, and generation time.
/api/stats/remedies returns rank-sorted remedies. The model never computes
statistics.
Coach
Coach is an optional OpenRouter selector/copywriter over server evidence and
server picks. A hardcoded ordered rule table is mandatory and always available.
A valid coach request always returns HTTP 200 with non-empty text and source
model, backup, or model_unparsed_fallback.
Pipeline: resolve current input; load last K truncated entries and today's daily row; compute evidence and picks; build system/user messages; start a trace; call OpenRouter; strictly parse the template; fall back on empty, failed, timed-out, or unparseable output; set flags; append/ring-trim trace; optionally persist coach metadata on the entry.
OpenRouter receives POST {BASE}/chat/completions, Bearer authorization,
JSON content type, X-Title: Habit Journal, configured model, messages,
temperature, token limit, and timeout.
Required model output:
LOOP: <tag>
FEELING: <word>
INTENSITY_GUESS: <1-10|n/a>
REMEDY: <one concrete action>
NEXT_BRICK: <A|B|C|D|E|S|specific>
DO_NOT: <one thing>
LINE: <12 words or fewer>
NOTE: <optional 20 words or fewer>
DATA_THIN: <true|false>
The prompt forbids invented numbers, canceling committed processes merely from fear, pity, and pep talks. Self-harm language triggers crisis redirection. Rules are ordered: crisis, court, rerun, rage_movie, daydream/fc, urge/corn, bully, shame, spain/admin, build/earn, home, loneliness, default. Non-crisis rules may substitute server pick number one.
Flags include DATA_THIN, PARSE_FAIL, TIMEOUT, EMPTY_MODEL,
NO_API_KEY, and OUT_OF_EVIDENCE.
Traces, debug, export, and operations
Every coach call stores request, current situation, truncated history, evidence, picks, brief hash/excerpt/full value according to config, both prompts, model settings, latency/status/error, raw output, parse, source, backup rule, flags, app version, and final text. Keep the newest configured N.
Authenticated debug endpoints list traces, return full traces, return one or the newest context-free Markdown paste bundle, preview prompts without a model call, and test coach with optional forced backup. The paste bundle contains: how an external AI should help, authoritative formulas, evidence, picks, current input, history, prompts, raw response, parse/source/flags, final text, and a reviewer checklist for invented numbers, ignored picks, format, fallback quality, evidence length, and weight tuning.
Authenticated exports provide entries JSONL, entries CSV, and daily JSONL. CSV has a fixed entry header and pipe-joins emotions and tags.
Health is public. Readiness reports writable data plus config presence. Authenticated settings status reports app name, coach configured state, model, data status, environment, and app version—never an API key.
Mount ./static at / only when present, after /api routes. Never mount the
data directory.
Default generic policy
- A: admin/critical checklist item
- B: environment/hygiene one act
- C: build/earn one ship
- D: logistics one line
- E: boundary one line then silence
- S: stop spiral — water/shower/sleep
Court is closed by default. Daydream requires a same-day brick. Urge policy is delay then brick, with a ceiling of one session per day expressed only in policy text.
Ordered implementation slices
- S0: Dockerfile, requirements, settings, paths, fs utilities, health/ready
- S1: password hashing, config store, auth, session, dependencies
- S2: entries CRUD JSONL
- S3: daily upsert, points, range band
- S4: pure statistics and stats routes
- S5: evidence and server picks
- S6: rule backup and offline coach
- S7: OpenRouter, parser, flags
- S8: traces and debug paste endpoints
- S9: export and settings status
- S10: seed brief, production checks, error polish
Do not begin S7 before S6. Tests must cover daily point edge cases, hand-calculated helped/rank math, and backup rule precedence; an authenticated temporary-data API test is optional.
Acceptance
Storage survives restart; good login sets a cookie and bad login is 401; unauthenticated data access is 401; daily and probability fixtures match; coach without a key and failed model calls return backup text; evidence and debug paste contain probabilities and server picks; paste contains formulas; authenticated export works; production OpenAPI is absent; and no secret is committed.
Explicit decisions are final: JSONL only, server-only math, probabilities and picks passed to the model, hardcoded backup, context-free debug bundles, single shared password, entry result as truth, and private life details only in the operator brief on disk.