Files
AgapHost/personal-sensing/schema.sql
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

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
);