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>
This commit is contained in:
269
mood/README.md
Normal file
269
mood/README.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# 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.
|
||||
3. ✅ Regular ingestion into local SQLite storage on Agap — done, this service.
|
||||
4. ✅ 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 1–5 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.sql` — `mood_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.py` — `init-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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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):
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user