Files
AgapHost/mood
alvis d37801806d services: add mood, moodtracker, overleaf, personal-sensing; update ollama
Compose and supporting code for four services that had been running or
prototyped without their config tracked here, per the repo convention that
agap_git holds the compose + config while application source lives in each
service's own Gitea repo.

Only placeholder credentials are included: mood/.env.example and
moodtracker/.env.example ship dummy values, and overleaf/variables.env carries
app name and feature flags only. Real values stay in Vaultwarden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 04:42:58 +00:00
..

mood — local archive of mood.alogins.net

Local SQLite copy of mood entries logged at mood.alogins.net, plus simple reports and a cross-source correlation hook. Kanboard Adolf #107 (data-pipeline half only — see "Scope" below).

Status: built and proven against the real live data (28 real entries pulled and queried successfully). Not yet deployed as a running service — docker compose up -d is a one-line handover, see "Deploy".


Scope of this build

Task #107 has four parts. This directory implements 1, 3, and 4 only:

  1. Investigate whether mood.alogins.net has an API/export — done, see below.
  2. Regular ingestion into local SQLite storage on Agap — done, this service.
  3. Simple reports/correlations over the stored data — done, query CLI.

2 is explicitly NOT built here: "proactive Matrix/Telegram reminder" is an outward-facing, scheduled message to the user. That capability belongs to the proactive-cadence framework (Kanboard Adolf #124), which is currently parked, tagged blocked, awaiting a human decision on whether to enable its cron at all (a sibling run already had to back out an unauthorized live crontab install — see #124 comments). Wiring a second scheduled outward message before that decision lands would repeat the same mistake.

Follow-up task to create: "Wire the mood.alogins.net proactive reminder" — depends on #124's cron being authorized. Once #124 is resolved, adding this reminder is small: a text-only Backlog-card-generation step reusing whatever executor #124 lands on, with the message Как день? Запиши в mood.alogins.net. See "Ready-to-hand-over reminder" below for a schedule a human can enable manually right now if they don't want to wait for #124.


1. Investigation: does mood.alogins.net have an API?

mood.alogins.net is not a third-party tracker — it's a small self-built Flask app already running on Agap:

  • Source: /home/alvis/moodtracker/app.py (+ templates/)
  • Container: moodtracker (compose at /home/alvis/moodtracker/docker-compose.yml)
  • Caddy: mood.alogins.net { reverse_proxy localhost:5177 } (/etc/caddy/Caddyfile)
  • DB: SQLite at /home/alvis/moodtracker/data/mood.db, one table entries (id, ts, mood, tags, note, affirmation), mood on a 15 scale, tags a JSON array of free-text strings, ts ISO8601 UTC.

It does have a JSON API, but it's session-cookie gated, not token-based:

  • POST /login (form username/password = AUTH_USER/AUTH_PASS env vars, currently plaintext in its own docker-compose.yml — pre-existing, not something this task introduced) → sets a Flask session cookie.
  • GET /api/history?limit=N, POST /api/log, DELETE /api/entry/<id> — all require that session cookie (@require_auth); no HTTP token/API-key.

Chosen ingestion path: read the SQLite file directly, not the HTTP API. data/mood.db is host-readable (644, owned by root:root, world-read bit set) — no credential needed. This is strictly more robust than replicating a cookie-login flow: it survives moodtracker adding/changing auth, needs no secret in this service at all, and is read-only by construction (bind-mounted :ro), so it can never corrupt or lock the live app's database.


3. Ingestion architecture

moodtracker's own SQLite file  ──(read-only bind mount)──►  src/mood_source.py
        /data/mood.db                                              │
                                                                     ▼
                                                            src/sync.py
                                                      (cursor + idempotent upsert)
                                                                     │
                                                                     ▼
                                                   local archive: mood_entries
                                          /mnt/dbs/mood/mood_archive.sqlite
                                                                     │
                                                                     ▼
                                                    src/cli.py query  (reports)

Mirrors the sibling googlefit service's shape (same author idiom, see agap_git/googlefit/):

  • schema.sqlmood_entries (PK source, source_id), sync_state (per-stream cursor), ingest_runs (audit log).
  • src/config.py — paths + tuning, no credentials (none needed).
  • src/mood_source.py — read-only reader for moodtracker's SQLite file.
  • src/store.py — schema init, idempotent UPSERTs, read/report queries.
  • src/sync.py — cursor-based incremental sync, isolated failure handling.
  • src/cli.pyinit-db | sync | query.

Why SQLite, not InfluxDB

Same reasoning as googlefit: single-user, a handful of manually-logged entries a week — trivial volume. Agap storage doctrine is SQLite-first. No extra always-on TSDB service for ~30 rows/month of data.

Idempotency / cursor

sync_state.last_synced_id is the high-water mark on moodtracker's own entries.id. Each run re-checks the last MOOD_OVERLAP_ROWS (default 3) already-synced ids too, as a cheap safety net — moodtracker currently has no edit endpoint (only insert + delete), so this is mostly redundant today but costs nothing.

Deleted-upstream entries are kept. If an entry is deleted via moodtracker's DELETE /api/entry/<id>, this archive does not remove its copy — it's an append-only journal by design, so history survives accidental or intentional deletes in the live app.

Proven against real data

$ python -m src.cli sync --once
{"synced": {"entries": 28, "errors": []}}
$ python -m src.cli query summary
{"entries": 28, "coverage": {"earliest": "2026-05-18T04:44:34...", "latest": "2026-07-07T05:49:34..."}, "avg_mood_all_time": 3.64, ...}

Re-running sync --once twice more produced zero row growth (idempotent). The source file's mtime was unchanged after every sync run (proves read-only). Also verified end-to-end through the built Docker image (docker build + docker run --rm ... sync --once against the real, live-mounted /home/alvis/moodtracker/data), then removed the test image/container — nothing was left running.

Test suite

$ python -m pytest tests/ -q
............                                                             [100%]
12 passed in 0.11s

Covers: upsert idempotency, conflict updates, sync-cursor high-water-mark behavior, a mock-moodtracker-schema DB driven through run_sync (first run, idempotent re-run, incremental pickup of a newly-inserted row, error handling when the source is missing, and read-only-ness), plus the report/correlation math below.


4. Reports / correlations

python -m src.cli query summary              # counts, coverage, freshness
python -m src.cli query entries --days 30     # raw recent entries
python -m src.cli query daily --days 30       # avg mood + entry count per day
python -m src.cli query tags --days 90        # avg mood per tag (min 2 occurrences)
python -m src.cli query correlate <csv> --days 90

daily and tags are the "mood over time" and "which tags coincide with low/high mood" reports from point 4. Real output against the live data (tags, 90-day window): lowest avg mood tags were exhausted (1.0, n=2), depressed (2.0, n=4); highest were happy (5.0, n=4), energetic (4.6, n=5).

Correlation hook (cross-source, not wired)

query correlate <csv_path> computes a Pearson r between the daily average mood and an arbitrary external daily series supplied as a plain day,value CSV (YYYY-MM-DD,float). This is a deliberate seam: it takes data, not a live connection to another service's DB or container, so wiring a real second source later (e.g. googlefit query metric heart_rate_avg or sleep minutes, once that service is deployed) is a one-line change — build the CSV/dict from that service's own read-only query CLI. This build does not reach into googlefit or any other service's DB, per the task's "leave hooks, don't wire other services" instruction.

Tested with a synthetic series (tests/test_store.py::test_correlate_with_series, near-perfect correlation asserted r > 0.99) and manually against the real mood data with a hand-built "hours slept" CSV (r = 0.933, n=5, small sample — illustrative only, not a real finding).


Deploy

mkdir -p /mnt/dbs/mood   # (needs sudo — /mnt/dbs is root-owned; see googlefit precedent)
cd /home/alvis/agap_git/mood
docker compose up -d --build

The container loops mood sync every MOOD_SYNC_INTERVAL_SECONDS (default 3600s / hourly — mood entries are logged manually, hourly polling of a local file is effectively free and gives fresh reports without any real cost).

One-off / cron alternative (no long-lived container):

docker compose run --rm mood-archive python -m src.cli sync --once

This was not started as a live service in this build — only proven via docker build + docker run --rm ... --once against a scratch data directory, then torn down. Bringing up the persistent restart: unless-stopped container is a one-line docker compose up -d --build for a human/operator to run.


Read tool for Adolf

docker compose run --rm mood-archive python -m src.cli query summary
docker compose run --rm mood-archive python -m src.cli query daily --days 14
docker compose run --rm mood-archive python -m src.cli query tags --days 90

JSON on stdout, read-only, no credentials.

Follow-up (adjacent, not in this task, same idiom as googlefit's README): promote query to a native agap-mcp tool once that server's active edit window (sibling task touching shared-mcp.json/openclaw.json) is clear.


Ready-to-hand-over reminder (point 2, NOT installed)

Per the scope note above, the proactive reminder is intentionally not built or scheduled here. If a human wants it live without waiting for #124's cron decision, here is a self-contained one-liner using the existing Telegram bot credentials already in Vaultwarden (TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID) — nothing new to build, nothing in this repo depends on it:

BW=/home/alvis/bin/bw
SESSION=$(env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
  NODE_TLS_REJECT_UNAUTHORIZED=0 $BW unlock "$BW_PASSWORD" --raw 2>/dev/null)
BOT=$(env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
  NODE_TLS_REJECT_UNAUTHORIZED=0 $BW get password "TELEGRAM_BOT_TOKEN" --session "$SESSION" 2>/dev/null)
CHAT=$(env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
  NODE_TLS_REJECT_UNAUTHORIZED=0 $BW get password "TELEGRAM_CHAT_ID" --session "$SESSION" 2>/dev/null)
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \
  curl -s -X POST "https://api.telegram.org/bot${BOT}/sendMessage" \
  -d "chat_id=${CHAT}" -d "text=Как день? Запиши в mood.alogins.net"

Proposed cron (a human adds this — not installed by this task, per the "never install an unattended cron on a live target" rule):

0 21 * * *  /home/alvis/agap_git/mood/scripts/remind.sh   # hypothetical path if built

No scripts/remind.sh exists yet — the command above is the full logic; if approved, wrapping it in a script + crontab line is a ~2-minute follow-up, but it is outward-facing (sends a message unattended) so it needs the same explicit human go-ahead #124 is waiting on, not a unilateral install by an agent.


Files

mood/
├── README.md
├── schema.sql
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example
├── .gitignore
├── src/
│   ├── config.py
│   ├── mood_source.py     # read-only reader for moodtracker's SQLite file
│   ├── store.py           # schema, upserts, reports, correlation hook
│   ├── sync.py            # cursor-based incremental sync
│   └── cli.py             # init-db | sync | query
└── tests/
    ├── test_store.py
    └── test_sync.py       # drives run_sync against a mock moodtracker DB

Nothing in this directory is committed to git — agap_git is a git repo but no git add/git commit was run for this task.