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>
96 lines
5.0 KiB
SQL
96 lines
5.0 KiB
SQL
-- 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
|
|
);
|