---
name: zimaos-productivity
description: Productivity apps on ZimaOS — DailyDose activity tracking, Zeitgeist Pomodoro + Anki, and CallKeep telephony. Data structures, APIs, cron-job motivation, and Anki import workflows.
version: 2.0.0
author: Hermes Agent
license: MIT
metadata:
  hermes:
    tags: [zimaos, productivity, dailydose, zeitgeist, anki, pomodoro, callkeep, tracker]
    related_skills: [zimaos-platform, zimaos-web-app]
---

# ZimaOS Productivity — DailyDose, Zeitgeist & CallKeep

Manage productivity apps running on ZimaOS: DailyDose (activity tracker), Zeitgeist (Pomodoro + Anki flashcards), and CallKeep (telephony/contact manager). Covers data structures, APIs, cron-job workflows, and Anki card imports.

---

## Part 1: DailyDose Activity Tracker

DailyDose is a PWA tracking Zeyd's daily activities. Runs locally with server sync via HTTP API.

### Architecture

- **Path:** `/media/HDD_1TB/DailyDose/`
- **Files:** `index.html` (complete app in one file), `manifest.json`, `icon.svg`, `nginx.conf`
- **API:** `http://localhost:8765/api/data` — GET reads all data, POST writes key-value pairs
- **Storage keys:** `dailydose_YYYY-MM-DD` for daily entries, `dd_custom_cats` for custom categories

### Categories

**Built-in** (in `BUILT_IN_CATS` array, ~line 878):
Schlafen, Lernen, Work, Hausarbeit, Kochen, Islam, Sport, Unterwegs, Körperpflege, Sonstiges

**Custom** stored in `dd_custom_cats` on server. Custom categories **override built-ins** (name match) in `buildCategoryMaps()`.

### Daily Defaults (`getDefaults()`, ~line 1031)

- **Islam:** 15 min daily, 45 min Fridays
- **Körperpflege:** 30 min daily
- **Unterwegs:** 1h Monday + Tuesday

Defaults only apply when no data exists for a day. Once saved, the stored data takes precedence.

### Modifying the Tracker

**New category:**
1. Patch `BUILT_IN_CATS` array in `index.html`
2. Add default in `getDefaults()` if desired
3. Check server for conflicting custom category names

**Existing day correction (direct API):**
```bash
curl -s http://localhost:8765/api/data | python3 -c "import json,sys; ..."
curl -s -X POST http://localhost:8765/api/data \
  -H 'Content-Type: application/json' \
  -d '{"dailydose_YYYY-MM-DD":"[{\"category\":\"...\",\"hours\":0.5}]"}'
```

### Cron-Job Motivation Reminders

Daily reminders via cronjob to encourage tracking:
- Schedule: `0 9,16,22 * * *` (three times daily)
- Style: German, friendly, motivating, not nagging, 1-2 sentences
- Address "Chef", vary wording, occasional emoji
- Day-aware: morning energetic, afternoon factual, evening relaxed

#### Goal-Based Motivation Cron Jobs

For goals like "120 minutes Lernen → Kopfhörer bestieren":
1. Python script queries DailyDose API, sums target category, outputs progress/motivation
2. Script to `~/.hermes/scripts/`, cron job with `no_agent: true`
3. Pause/delete when goal reached

See `scripts/learn_check.py` for a standalone motivation checker. Usage:
```bash
python3 scripts/learn_check.py [target_minutes] [category]
# Defaults: 120 minutes, "Lernen"
```

#### Morning Briefing Cronjob

The daily morning briefing combines Calendar (Google), CallKeep, DailyDose, and weather into a personalized daily overview. The complete prompt template with structure, style rules, and examples is at `references/morgenbriefing-template.md`.

Briefing data sources:
1. Weather: `curl -s "wttr.in/Vienna?format=%C+%t+%h+%w"`
2. Google Calendar: `python $GAPI calendar list --all-calendars --start HEUTE --end MORGEN`
3. CallKeep: `curl -s http://localhost:3002/api/calls?date=HEUTE` + history (last 2 weeks)
4. DailyDose: `dailydose_YYYY-MM-DD` — requires double JSON parsing (values are strings!)

### Critical Pitfall: Double JSON Encoding

**ALL values from the Zeitgeist/DailyDose API are JSON-strings — doppelt parsen!**

```python
for k, v in data.items():
    if isinstance(v, str) and v.strip().startswith(('{', '[')):
        data[k] = json.loads(v)
```

Without this, accessing `data['zeitgeist-v2']['2026-05-22']` throws `AttributeError: 'str' object has no attribute 'get'`.

### Anki FSRS Algorithm

Zeitgeist's Anki uses a custom FSRS-4.5 implementation in `anki.html`. For exact constants, scheduling rules, formulas, and migration behaviour see `references/zeitgeist-anki-fsrs.md`.

---

## Part 2: Zeitgeist (Pomodoro + Anki)

## Part 2: Zeitgeist (Pomodoro + Anki)

Zeitgeist is a daily activity tracker with integrated Anki system (FSRS-based) running in a Docker container on ZimaOS.

### Infrastructure

| Property | Value |
|---|---|
| Container | `Zeitgeist` (Image: `node:20-alpine`) |
| Port | `8765` (intern 3000) |
| Data | `/media/HDD_1TB/Zeitgeist` → container `/app` |
| Data file | `/media/HDD_1TB/Zeitgeist/data/data.json` |
| Compose | `/var/lib/casaos/apps/Zeitgeist/docker-compose.yml` |

### Data Structure (data.json)

| Key | Content |
|---|---|
| `zeitgeist-v2` | Daily Pomodoro sessions (Date → [10 slots × 25min]) |
| `zeitgeist-v2-notes` | Daily notes (Date → [10 strings]) |
| `zeitgeist-work-v2` | Work Pomodoro tracker |
| `zeitgeist-anki-v2` | **Anki data** — JSON-string (double encoded!) |

### Anki Substructure

The `zeitgeist-anki-v2` value is a **JSON-string**, not an object — must decode twice.

Zeitgeist stores cards **directly as arrays in the deck**, not as separate `cards`/`notes` Maps:

```json
{
  "decks": {
    "<deckId>": {
      "id": "<deckId>", "name": "Deck-Name", "color": "#HEX",
      "cards": [
        {
          "id": "<cardId>", "front": "...", "back": "...",
          "state": "new" | "learning" | "review" | "relearn",
          "interval": 0, "stability": null, "difficulty": null,
          "lastReview": null, "dueDate": null, "lapses": 0, "reviews": 0,
          "learningStep": 0, "easeFactor": 2.5
        }
      ]
    }
  }
}
```

**Card states:** `new` → `learning` → `review` → `relearn` (after lapse)

**ID generation:** `Date.now().toString(36) + Math.random().toString(36).slice(2,6)` — base36 timestamp + 4 random chars.

### Importing Anki Cards from .apkg/.colpkg

.apkg files are internally ZIP files containing:
- `collection.anki2` — older SQLite database (often just a placeholder in .colpkg)
- `collection.anki21b` — **Zstandard-compressed** SQLite (real data in newer packages)
- `media` — media map
- `meta` — metadata

**Critical:** The system does not accept `.apkg` attachments. Workaround: rename to `.zip` first.

**Zstandard decompression:**
```bash
pip install zstandard
```
```python
import zstandard as zstd
with open('collection.anki21b', 'rb') as f:
    compressed = f.read()
dctx = zstd.ZstdDecompressor()
decompressed = dctx.decompressobj().decompress(compressed)
# → SQLite database
```

**SQLite schema (Anki collection):**
- `decks` — `id`, `name`
- `notetypes` — `id`, `name`
- `notes` — `id`, `guid`, `mid`, `flds` (Front\x1fBack), `tags`
- `cards` — `id`, `nid`, `did`, `ord`, `queue`, `due`, `ivl`, `factor`, `reps`, `lapses`

Field separator in `notes.flds`: `\x1f` (ASCII Unit Separator).

**Import workflow (complete):**
1. Rename `.apkg` → `.zip`, extract, decompress `collection.anki21b`
2. SQLite read: split `notes.flds` by `\x1f` into Front/Back
3. Generate Zeitgeist cards (ID via `uid()`, state `new`, FSRS defaults)
4. Append to `deck.cards[]`
5. Write back: `zeitgeist-anki-v2` = `json.dumps(anki_object)`
6. Atomic write: tmp file → `os.replace()`; backup first

**Permissions:** `/media/HDD_1TB/Zeitgeist/data/` is root-owned. Write to `/tmp/` first, then `sudo mv`.

### Writing to data.json

```python
import json, os, shutil

# 1. Backup
shutil.copy2(ZG_DATA, f'/tmp/zg-backup-pre-{timestamp}.json')

# 2. Modify data
# ... changes ...

# 3. Atomic write
tmp_path = ZG_DATA + '.tmp'
with open(tmp_path, 'w') as f:
    json.dump(data, f, ensure_ascii=False)
os.replace(tmp_path, ZG_DATA)  # atomic
```

### Troubleshooting

- **JSON parse error:** The Anki value is double-encoded. `json.load()` on file, then `json.loads()` on the string.
- **Changes not visible:** Zeitgeist reads `data.json` fresh on every request. No cache.
- **PermissionError:** Write to `/tmp/`, then `sudo mv`.
- **Container reset:** `cd /var/lib/casaos/apps/Zeitgeist && DOCKER_CONFIG=/tmp/docker-config docker compose up -d --force-recreate`
- **`collection.anki2` contains only placeholder:** Real data is in `collection.anki21b` (compressed).

See `scripts/import-apkg.py` for the complete tested import script.

### Day-Card Color Modes (Rainbow & Gold)

The main Zeitgeist tracker (`zeitgeist.html`) applies visual effects to day cards based on learning progress. Thresholds are evaluated in `cardHTML()` (line 1144) and `refreshCard()` (line 1261):

| Modus | Bedingung | CSS-Klasse | Effekt |
|-------|-----------|------------|--------|
| **Normal** | 0–180 min | (keine) | Standard-Karte |
| 🌈 **Rainbow** | **>180 Minuten** UND **<10 Slots gefüllt** | `is-rainbow` | Animierter Regenbogen-Gradient (8s-Loop, `rainbow-flow` keyframes) |
| ✨ **Gold** | **10/10 Slots gefüllt** (Minuten egal) | `is-gold` | Gold-Shimmer + Glow-Animation — **überschreibt Rainbow** |

**Entscheidungslogik (Zeile 1144–1145):**
```js
const isGold    = used === PER_DAY;        // 10/10 Einheiten
const isRainbow = total > 180 && !isGold;  // >180 min, aber nicht voll
```

**Wichtige Details:**
- Gold hat **Priorität** über Rainbow (`isGold ? 'is-gold' : isRainbow ? 'is-rainbow' : ''`)
- Gold triggert bei 10/10 **unabhängig von der Minutenzahl** — auch 10×1 Min Free-Time = Gold
- Rainbow triggert ab **181 Minuten** (nicht 180)
- Rainbow-Animation läuft nur auf der **heutigen** Karte (`.is-today`). Archiv-Karten zeigen statischen Snapshot
- Gold-Karten im Archiv haben keinen Shimmer (`.is-gold:not(.is-today)::after { animation: none; opacity: 0; }`)
- Beide Modi haben angepasste Dark-Mode-Varianten

**Beispiele:**
- 7×25 = 175 min, 7/10 → Normal
- 8×25 = 200 min, 8/10 → 🌈 Rainbow
- 9×25 = 225 min, 9/10 → 🌈 Rainbow
- 10×25 = 250 min, 10/10 → ✨ Gold
- 10×1 = 10 min, 10/10 → ✨ Gold

### FSRS Algorithm Reference

The embedded Anki scheduler is a custom JavaScript implementation of **FSRS-4.5** (not SM-2). For the exact constants, state machine, update formulas, and migration logic used in `anki.html`, see `references/zeitgeist-anki-fsrs.md`.

---

## Part 3: CallKeep Telephony Manager

CallKeep tracks phone calls and contact frequencies. See `references/callkeep-db-schema.md` for full SQLite schema.

### Quick Reference: Database Schema

**Tables:** `contacts`, `calls`

**Key `contacts` fields:** `id`, `name`, `nickname`, `relationship`, `frequency` (Täglich/Wöchentlich/etc.), `priority` (1-5), `birthday`, `photo_url`

**Key `calls` fields:** `id`, `contact_id`, `direction` (Ausgehend/Eingehend), `duration_minutes`, `topics`, `mood`, `follow_up_needed`, `called_at`, `next_call_at`, `summary`

**Frequency → days mapping:**
- Täglich=1, Wöchentlich=7, Alle 2 Wochen=14, Monatlich=30, Alle 2 Monate=60, Vierteljährlich=90, Halbjährlich=180, Jährlich=365

See `scripts/callkeep-overdue.py` for a script listing overdue/today calls.

---

## Shared ZimaOS Productivity Pitfalls

- **Double JSON encoding:** DailyDose, Zeitgeist, and any app using `data.json` stores values as JSON-strings. Always check `isinstance(v, str) && v.strip().startswith(('{', '['))` before parsing.
- **Root-owned /media/ paths:** `/media/HDD_1TB/` files are `root:root`. All writes require `sudo`. Agent runs as `az-a`.
- **Cron-job prompts must be self-contained:** No chat context available. Include all API endpoints and parsing logic in the prompt.
- **Container restarts:** CasaOS-managed apps should use `docker compose up -d --force-recreate` from their `/var/lib/casaos/apps/<name>/` directory.

## Support Files

- `references/morgenbriefing-template.md` — Complete cronjob prompt template for daily morning briefing
- `references/callkeep-db-schema.md` — CallKeep SQLite database schema
- `references/zeitgeist-anki-fsrs.md` — FSRS-4.5 scheduler constants, formulas, and state machine used in `anki.html`
- `references/zeitgeist-anki-ui.md` — Anki UI architecture, 3D-flip mechanism, priority-glow pitfalls, Dojo-mode access, mobile overrides
- `scripts/learn_check.py` — Standalone DailyDose motivation/progress checker
- `scripts/import-apkg.py` — Complete Anki → Zeitgeist card import script
