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:
95
personal-sensing/schema.sql
Normal file
95
personal-sensing/schema.sql
Normal file
@@ -0,0 +1,95 @@
|
||||
-- Google Fit local archive — SQLite schema (source-agnostic).
|
||||
--
|
||||
-- Design note: one normalized store serves BOTH ingestion paths — the Google Fit
|
||||
-- REST API adapter and the Google Takeout importer (deprecation hedge, see README).
|
||||
-- Time-series points, workout/sleep sessions, and sleep stages each get a table;
|
||||
-- every write is an idempotent UPSERT keyed on the natural Fit identity of the row,
|
||||
-- so re-running a sync over an overlapping window never duplicates data.
|
||||
--
|
||||
-- Why SQLite and not InfluxDB: single-user, daily-cadence health data is low volume
|
||||
-- (thousands of rows/day at most); the Agap/OpenClaw storage doctrine is SQLite-only;
|
||||
-- and a normalized relational store answers the "sessions + metadata + series" query
|
||||
-- mix better than a pure TSDB would. Adding an always-on InfluxDB service would be
|
||||
-- operational cost with no payoff at this scale. (Task listed both as options.)
|
||||
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
-- Time-series metric points: steps, heart rate, calories, distance, weight, etc.
|
||||
-- One row per (metric, interval, source). `metric` is our normalized name
|
||||
-- (e.g. 'steps', 'heart_rate_avg'), decoupled from Google's data type strings.
|
||||
CREATE TABLE IF NOT EXISTS data_points (
|
||||
metric TEXT NOT NULL, -- normalized: steps, heart_rate_avg, calories, ...
|
||||
data_type_name TEXT NOT NULL, -- raw Google Fit data type (provenance)
|
||||
start_ns INTEGER NOT NULL, -- interval start, epoch nanoseconds
|
||||
end_ns INTEGER NOT NULL, -- interval end, epoch nanoseconds
|
||||
start_time TEXT NOT NULL, -- ISO8601 UTC (human/SQL friendly)
|
||||
end_time TEXT NOT NULL,
|
||||
value_int INTEGER, -- populated for integer metrics
|
||||
value_float REAL, -- populated for float metrics
|
||||
value_str TEXT, -- populated for string/enum metrics
|
||||
unit TEXT, -- count, bpm, kcal, m, kg, min, ...
|
||||
data_source_id TEXT NOT NULL DEFAULT '', -- originating Fit stream (may be '')
|
||||
source TEXT NOT NULL DEFAULT 'ha', -- ha | takeout
|
||||
ingested_at TEXT NOT NULL,
|
||||
PRIMARY KEY (metric, start_ns, end_ns, data_source_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_dp_metric_time ON data_points (metric, start_ns);
|
||||
CREATE INDEX IF NOT EXISTS idx_dp_start_time ON data_points (start_time);
|
||||
|
||||
-- Workouts / activities / sleep sessions (com.google.session).
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY, -- Fit session id (stable, upsert key)
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
activity_type INTEGER, -- Fit activity type enum
|
||||
activity_name TEXT, -- resolved label (e.g. 'Running', 'Sleep')
|
||||
start_ns INTEGER NOT NULL,
|
||||
end_ns INTEGER NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
modified_ns INTEGER,
|
||||
application TEXT, -- packageName that wrote the session
|
||||
source TEXT NOT NULL DEFAULT 'ha',
|
||||
raw_json TEXT, -- full session payload for reprocessing
|
||||
ingested_at TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_start ON sessions (start_ns);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions (activity_type);
|
||||
|
||||
-- Sleep stage segments (com.google.sleep.segment). Stage enum decoded to a label.
|
||||
CREATE TABLE IF NOT EXISTS sleep_segments (
|
||||
start_ns INTEGER NOT NULL,
|
||||
end_ns INTEGER NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
stage INTEGER NOT NULL, -- raw Fit sleep-stage enum
|
||||
stage_name TEXT NOT NULL, -- awake, light, deep, rem, out_of_bed, sleep
|
||||
data_source_id TEXT NOT NULL DEFAULT '',
|
||||
source TEXT NOT NULL DEFAULT 'ha',
|
||||
ingested_at TEXT NOT NULL,
|
||||
PRIMARY KEY (start_ns, end_ns, data_source_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sleep_start ON sleep_segments (start_ns);
|
||||
|
||||
-- Incremental-sync cursor per stream. `last_synced_ns` is the high-water mark
|
||||
-- (max end_ns fetched); the next run resumes from there minus a small overlap.
|
||||
CREATE TABLE IF NOT EXISTS sync_state (
|
||||
stream_key TEXT PRIMARY KEY, -- metric name, 'sessions', or 'sleep'
|
||||
last_synced_ns INTEGER NOT NULL DEFAULT 0,
|
||||
last_run_at TEXT,
|
||||
last_status TEXT, -- ok | error
|
||||
last_error TEXT
|
||||
);
|
||||
|
||||
-- Audit log of ingestion runs (observability; Zabbix can later read staleness).
|
||||
CREATE TABLE IF NOT EXISTS ingest_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
started_at TEXT NOT NULL,
|
||||
finished_at TEXT,
|
||||
status TEXT, -- ok | error
|
||||
points_upserted INTEGER DEFAULT 0,
|
||||
sessions_upserted INTEGER DEFAULT 0,
|
||||
segments_upserted INTEGER DEFAULT 0,
|
||||
error TEXT
|
||||
);
|
||||
Reference in New Issue
Block a user