hae-vault 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/CLAUDE.md +220 -0
- package/README.md +206 -0
- package/SKILL.md +60 -0
- package/dist/cli/dashboard.d.ts +3 -0
- package/dist/cli/dashboard.d.ts.map +1 -0
- package/dist/cli/dashboard.js +206 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/import.d.ts +3 -0
- package/dist/cli/import.d.ts.map +1 -0
- package/dist/cli/import.js +78 -0
- package/dist/cli/import.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +31 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/info.d.ts +5 -0
- package/dist/cli/info.d.ts.map +1 -0
- package/dist/cli/info.js +34 -0
- package/dist/cli/info.js.map +1 -0
- package/dist/cli/metrics.d.ts +3 -0
- package/dist/cli/metrics.d.ts.map +1 -0
- package/dist/cli/metrics.js +20 -0
- package/dist/cli/metrics.js.map +1 -0
- package/dist/cli/query.d.ts +3 -0
- package/dist/cli/query.d.ts.map +1 -0
- package/dist/cli/query.js +18 -0
- package/dist/cli/query.js.map +1 -0
- package/dist/cli/serve.d.ts +3 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +19 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/sleep.d.ts +3 -0
- package/dist/cli/sleep.d.ts.map +1 -0
- package/dist/cli/sleep.js +19 -0
- package/dist/cli/sleep.js.map +1 -0
- package/dist/cli/summary.d.ts +3 -0
- package/dist/cli/summary.d.ts.map +1 -0
- package/dist/cli/summary.js +53 -0
- package/dist/cli/summary.js.map +1 -0
- package/dist/cli/trends.d.ts +3 -0
- package/dist/cli/trends.d.ts.map +1 -0
- package/dist/cli/trends.js +77 -0
- package/dist/cli/trends.js.map +1 -0
- package/dist/cli/watch.d.ts +12 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +89 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/cli/workouts.d.ts +3 -0
- package/dist/cli/workouts.d.ts.map +1 -0
- package/dist/cli/workouts.js +19 -0
- package/dist/cli/workouts.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -0
- package/dist/db/importLog.d.ts +5 -0
- package/dist/db/importLog.d.ts.map +1 -0
- package/dist/db/importLog.js +10 -0
- package/dist/db/importLog.js.map +1 -0
- package/dist/db/metrics.d.ts +4 -0
- package/dist/db/metrics.d.ts.map +1 -0
- package/dist/db/metrics.js +14 -0
- package/dist/db/metrics.js.map +1 -0
- package/dist/db/schema.d.ts +5 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +100 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/sleep.d.ts +4 -0
- package/dist/db/sleep.d.ts.map +1 -0
- package/dist/db/sleep.js +13 -0
- package/dist/db/sleep.js.map +1 -0
- package/dist/db/workouts.d.ts +4 -0
- package/dist/db/workouts.d.ts.map +1 -0
- package/dist/db/workouts.js +11 -0
- package/dist/db/workouts.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parse/metrics.d.ts +17 -0
- package/dist/parse/metrics.d.ts.map +1 -0
- package/dist/parse/metrics.js +33 -0
- package/dist/parse/metrics.js.map +1 -0
- package/dist/parse/sleep.d.ts +23 -0
- package/dist/parse/sleep.d.ts.map +1 -0
- package/dist/parse/sleep.js +58 -0
- package/dist/parse/sleep.js.map +1 -0
- package/dist/parse/time.d.ts +4 -0
- package/dist/parse/time.d.ts.map +1 -0
- package/dist/parse/time.js +41 -0
- package/dist/parse/time.js.map +1 -0
- package/dist/parse/workouts.d.ts +17 -0
- package/dist/parse/workouts.d.ts.map +1 -0
- package/dist/parse/workouts.js +24 -0
- package/dist/parse/workouts.js.map +1 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.d.ts.map +1 -0
- package/dist/server/app.js +39 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/ingest.d.ts +15 -0
- package/dist/server/ingest.d.ts.map +1 -0
- package/dist/server/ingest.js +41 -0
- package/dist/server/ingest.js.map +1 -0
- package/dist/types/hae.d.ts +103 -0
- package/dist/types/hae.d.ts.map +1 -0
- package/dist/types/hae.js +2 -0
- package/dist/types/hae.js.map +1 -0
- package/dist/util/zip.d.ts +3 -0
- package/dist/util/zip.d.ts.map +1 -0
- package/dist/util/zip.js +24 -0
- package/dist/util/zip.js.map +1 -0
- package/docs/COMMANDS.md +315 -0
- package/docs/plans/2026-02-18-hae-vault-initial-implementation.md +2015 -0
- package/docs/plans/2026-02-18-readme-dashboard-design.md +213 -0
- package/docs/plans/2026-02-18-readme-dashboard-plan.md +1306 -0
- package/docs/plans/2026-02-18-zip-env-watch-design.md +213 -0
- package/docs/plans/2026-02-18-zip-env-watch.md +966 -0
- package/package.json +57 -0
- package/src/cli/dashboard.ts +242 -0
- package/src/cli/import.ts +85 -0
- package/src/cli/index.ts +32 -0
- package/src/cli/info.ts +36 -0
- package/src/cli/metrics.ts +20 -0
- package/src/cli/query.ts +17 -0
- package/src/cli/serve.ts +18 -0
- package/src/cli/sleep.ts +19 -0
- package/src/cli/summary.ts +58 -0
- package/src/cli/trends.ts +103 -0
- package/src/cli/watch.ts +111 -0
- package/src/cli/workouts.ts +19 -0
- package/src/config.ts +28 -0
- package/src/db/importLog.ts +18 -0
- package/src/db/metrics.ts +15 -0
- package/src/db/schema.ts +105 -0
- package/src/db/sleep.ts +15 -0
- package/src/db/workouts.ts +13 -0
- package/src/index.ts +4 -0
- package/src/parse/metrics.ts +50 -0
- package/src/parse/sleep.ts +82 -0
- package/src/parse/time.ts +43 -0
- package/src/parse/workouts.ts +42 -0
- package/src/server/app.ts +46 -0
- package/src/server/ingest.ts +68 -0
- package/src/types/hae.ts +94 -0
- package/src/util/zip.ts +24 -0
- package/tests/cli-watch.test.ts +64 -0
- package/tests/db-import-log.test.ts +40 -0
- package/tests/db-metrics.test.ts +44 -0
- package/tests/db-schema.test.ts +55 -0
- package/tests/db-sleep.test.ts +36 -0
- package/tests/db-workouts.test.ts +34 -0
- package/tests/ingest.test.ts +99 -0
- package/tests/parse-metrics.test.ts +55 -0
- package/tests/parse-sleep.test.ts +65 -0
- package/tests/parse-time.test.ts +48 -0
- package/tests/parse-workouts.test.ts +43 -0
- package/tests/types.test.ts +27 -0
- package/tests/util-zip.test.ts +46 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,1306 @@
|
|
|
1
|
+
# hae-vault README, Dashboard & CLI Polish Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a rich terminal dashboard, trends, colored summary to hae-vault CLI; write full README + COMMANDS docs; configure package.json and push to GitHub.
|
|
6
|
+
|
|
7
|
+
**Architecture:** No new dependencies. Pure string formatting in TypeScript. Three new/modified CLI files (`dashboard.ts`, `trends.ts`, `summary.ts`). All queries use existing `openDb()` + better-sqlite3. Display logic is self-contained — no shared formatter module (YAGNI).
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, better-sqlite3, Commander.js, Node.js 22+
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
- **CLI binary:** `hvault` (package `hae-vault`)
|
|
16
|
+
- **DB path:** `~/.hae-vault/health.db` (via `openDb()` from `src/db/schema.ts`)
|
|
17
|
+
- **Build:** `npm run build` (tsc → dist/); dev run: `npm run dev -- <cmd>`
|
|
18
|
+
- **Test:** `npm test` (tsx --test tests/**/*.test.ts)
|
|
19
|
+
- **Schema key facts:**
|
|
20
|
+
- `metrics(ts, date, metric, qty, min, avg, max, units, source, target)`
|
|
21
|
+
- `sleep(date, sleep_start, sleep_end, core_h, deep_h, rem_h, awake_h, asleep_h, in_bed_h, source)`
|
|
22
|
+
- `workouts(ts, date, name, duration_s, calories_kj, distance, distance_unit, avg_hr, max_hr, target)`
|
|
23
|
+
- `sync_log(received_at, target, metrics_count, workouts_count)`
|
|
24
|
+
- `import_log(filename, file_hash, imported_at, metrics_added, sleep_added, workouts_added)`
|
|
25
|
+
- Sleep light hours = `asleep_h - deep_h - rem_h` (no separate column)
|
|
26
|
+
- `duration_s` is seconds; `calories_kj` is kilojoules (÷ 4.184 = kcal)
|
|
27
|
+
- **Existing CLI files:** `src/cli/{index,serve,import,watch,metrics,sleep,workouts,summary,query,info}.ts`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Task 1: Update package.json
|
|
32
|
+
|
|
33
|
+
**Files:**
|
|
34
|
+
- Modify: `package.json`
|
|
35
|
+
|
|
36
|
+
**Step 1: Add metadata fields**
|
|
37
|
+
|
|
38
|
+
Edit `package.json` — add these fields after `"license": "MIT"`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/mrkhachaturov/hae-vault.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/mrkhachaturov/hae-vault#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/mrkhachaturov/hae-vault/issues"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"apple-health",
|
|
51
|
+
"health-auto-export",
|
|
52
|
+
"sqlite",
|
|
53
|
+
"cli",
|
|
54
|
+
"hvault",
|
|
55
|
+
"sleep",
|
|
56
|
+
"hrv",
|
|
57
|
+
"steps",
|
|
58
|
+
"fitness",
|
|
59
|
+
"hae"
|
|
60
|
+
],
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Final `package.json` (complete):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"name": "hae-vault",
|
|
68
|
+
"version": "0.1.0",
|
|
69
|
+
"description": "CLI + HTTP server for Apple Health data from Health Auto Export",
|
|
70
|
+
"type": "module",
|
|
71
|
+
"bin": {
|
|
72
|
+
"hvault": "./dist/index.js"
|
|
73
|
+
},
|
|
74
|
+
"main": "./dist/index.js",
|
|
75
|
+
"scripts": {
|
|
76
|
+
"dev": "tsx src/index.ts",
|
|
77
|
+
"build": "tsc",
|
|
78
|
+
"start": "node dist/index.js",
|
|
79
|
+
"prepare": "npm run build",
|
|
80
|
+
"test": "tsx --test tests/**/*.test.ts"
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=22.0.0"
|
|
84
|
+
},
|
|
85
|
+
"keywords": [
|
|
86
|
+
"apple-health",
|
|
87
|
+
"health-auto-export",
|
|
88
|
+
"sqlite",
|
|
89
|
+
"cli",
|
|
90
|
+
"hvault",
|
|
91
|
+
"sleep",
|
|
92
|
+
"hrv",
|
|
93
|
+
"steps",
|
|
94
|
+
"fitness",
|
|
95
|
+
"hae"
|
|
96
|
+
],
|
|
97
|
+
"author": "Ruben Khachaturov <mr.kha4a2rov@protonmail.com>",
|
|
98
|
+
"license": "MIT",
|
|
99
|
+
"repository": {
|
|
100
|
+
"type": "git",
|
|
101
|
+
"url": "https://github.com/mrkhachaturov/hae-vault.git"
|
|
102
|
+
},
|
|
103
|
+
"homepage": "https://github.com/mrkhachaturov/hae-vault#readme",
|
|
104
|
+
"bugs": {
|
|
105
|
+
"url": "https://github.com/mrkhachaturov/hae-vault/issues"
|
|
106
|
+
},
|
|
107
|
+
"dependencies": {
|
|
108
|
+
"adm-zip": "^0.5.16",
|
|
109
|
+
"better-sqlite3": "^12.6.2",
|
|
110
|
+
"commander": "^12.1.0",
|
|
111
|
+
"dotenv": "^17.3.1",
|
|
112
|
+
"express": "^4.21.0"
|
|
113
|
+
},
|
|
114
|
+
"devDependencies": {
|
|
115
|
+
"@types/adm-zip": "^0.5.7",
|
|
116
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
117
|
+
"@types/express": "^4.17.21",
|
|
118
|
+
"@types/node": "^22.10.0",
|
|
119
|
+
"tsx": "^4.19.0",
|
|
120
|
+
"typescript": "^5.7.0"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Step 2: Verify JSON is valid**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
node -e "JSON.parse(require('fs').readFileSync('package.json','utf8')); console.log('valid')"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Expected: `valid`
|
|
132
|
+
|
|
133
|
+
**Step 3: Commit**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git add package.json
|
|
137
|
+
git commit -m "chore: add repository/homepage/bugs/keywords to package.json"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Task 2: Create `src/cli/dashboard.ts`
|
|
143
|
+
|
|
144
|
+
**Files:**
|
|
145
|
+
- Create: `src/cli/dashboard.ts`
|
|
146
|
+
- Modify: `src/cli/index.ts`
|
|
147
|
+
|
|
148
|
+
**Step 1: Create the file**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// src/cli/dashboard.ts
|
|
152
|
+
import { Command } from 'commander';
|
|
153
|
+
import { openDb } from '../db/schema.js';
|
|
154
|
+
|
|
155
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function pad(s: string, width: number): string {
|
|
158
|
+
return s.padEnd(width);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function fmt1(n: number | null | undefined): string {
|
|
162
|
+
if (n == null) return '—';
|
|
163
|
+
return n.toFixed(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function fmtInt(n: number | null | undefined): string {
|
|
167
|
+
if (n == null) return '—';
|
|
168
|
+
return Math.round(n).toLocaleString();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function sec2min(s: number | null | undefined): string {
|
|
172
|
+
if (s == null) return '—';
|
|
173
|
+
return `${Math.round(s / 60)}min`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function kj2kcal(kj: number | null | undefined): string {
|
|
177
|
+
if (kj == null) return '—';
|
|
178
|
+
return `${Math.round(kj / 4.184)} kcal`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function arrow(first: number, last: number): string {
|
|
182
|
+
const delta = last - first;
|
|
183
|
+
if (Math.abs(delta) < 0.01 * Math.abs(first || 1)) return '→';
|
|
184
|
+
return delta > 0 ? '↑' : '↓';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ruler(label: string, width = 43): string {
|
|
188
|
+
const inner = `── ${label} `;
|
|
189
|
+
return inner + '─'.repeat(Math.max(0, width - inner.length));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function workoutEmoji(name: string): string {
|
|
193
|
+
const n = name.toLowerCase();
|
|
194
|
+
if (n.includes('run')) return '🏃';
|
|
195
|
+
if (n.includes('walk')) return '🚶';
|
|
196
|
+
if (n.includes('cycl') || n.includes('bike')) return '🚴';
|
|
197
|
+
if (n.includes('swim')) return '🏊';
|
|
198
|
+
if (n.includes('yoga')) return '🧘';
|
|
199
|
+
if (n.includes('strength') || n.includes('weight') || n.includes('lift')) return '🏋️';
|
|
200
|
+
if (n.includes('hike')) return '🥾';
|
|
201
|
+
return '🏅';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function latestMetric(
|
|
205
|
+
db: ReturnType<typeof openDb>,
|
|
206
|
+
metricName: string,
|
|
207
|
+
days = 7
|
|
208
|
+
): number | null {
|
|
209
|
+
const since = new Date();
|
|
210
|
+
since.setDate(since.getDate() - days);
|
|
211
|
+
const row = db.prepare(
|
|
212
|
+
`SELECT qty FROM metrics WHERE metric = ? AND date >= ? AND qty IS NOT NULL ORDER BY ts DESC LIMIT 1`
|
|
213
|
+
).get(metricName, since.toISOString().slice(0, 10)) as { qty: number } | undefined;
|
|
214
|
+
return row?.qty ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function dailyAvgs(
|
|
218
|
+
db: ReturnType<typeof openDb>,
|
|
219
|
+
metricName: string,
|
|
220
|
+
days: number
|
|
221
|
+
): number[] {
|
|
222
|
+
const since = new Date();
|
|
223
|
+
since.setDate(since.getDate() - days);
|
|
224
|
+
const rows = db.prepare(
|
|
225
|
+
`SELECT date, AVG(qty) as avg_qty FROM metrics
|
|
226
|
+
WHERE metric = ? AND date >= ? AND qty IS NOT NULL
|
|
227
|
+
GROUP BY date ORDER BY date ASC`
|
|
228
|
+
).all(metricName, since.toISOString().slice(0, 10)) as { date: string; avg_qty: number }[];
|
|
229
|
+
return rows.map(r => r.avg_qty);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sleepDailyAvgs(
|
|
233
|
+
db: ReturnType<typeof openDb>,
|
|
234
|
+
days: number
|
|
235
|
+
): number[] {
|
|
236
|
+
const since = new Date();
|
|
237
|
+
since.setDate(since.getDate() - days);
|
|
238
|
+
const rows = db.prepare(
|
|
239
|
+
`SELECT asleep_h FROM sleep WHERE date >= ? AND asleep_h IS NOT NULL ORDER BY date ASC`
|
|
240
|
+
).all(since.toISOString().slice(0, 10)) as { asleep_h: number }[];
|
|
241
|
+
return rows.map(r => r.asleep_h);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function trendLine(
|
|
245
|
+
values: number[],
|
|
246
|
+
label: string,
|
|
247
|
+
unit: string,
|
|
248
|
+
round = false
|
|
249
|
+
): string {
|
|
250
|
+
if (values.length < 2) return '';
|
|
251
|
+
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
252
|
+
const first = values[0];
|
|
253
|
+
const last = values[values.length - 1];
|
|
254
|
+
const dir = arrow(first, last);
|
|
255
|
+
const fmt = round ? fmtInt : fmt1;
|
|
256
|
+
return ` ${pad(label + ':', 14)} ${fmt(first)} → ${fmt(last)}${unit} ${dir} (avg ${fmt(avg)})`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── command ───────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
export const dashboardCommand = new Command('dashboard')
|
|
262
|
+
.description('Terminal dashboard: sleep, activity, heart health, workouts, trends')
|
|
263
|
+
.option('--days <n>', 'Trend window in days', '7')
|
|
264
|
+
.option('--json', 'Output raw JSON')
|
|
265
|
+
.action((opts) => {
|
|
266
|
+
const db = openDb();
|
|
267
|
+
const trendDays = parseInt(opts.days, 10);
|
|
268
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
269
|
+
|
|
270
|
+
// ── sleep (last night) ─────────────────────────────────────────────────
|
|
271
|
+
const sleep = db.prepare(
|
|
272
|
+
`SELECT * FROM sleep ORDER BY date DESC LIMIT 1`
|
|
273
|
+
).get() as {
|
|
274
|
+
date: string; asleep_h: number | null; in_bed_h: number | null;
|
|
275
|
+
deep_h: number | null; rem_h: number | null; awake_h: number | null;
|
|
276
|
+
source: string | null;
|
|
277
|
+
} | undefined;
|
|
278
|
+
|
|
279
|
+
// ── activity (today, fallback last 2 days) ─────────────────────────────
|
|
280
|
+
const steps = latestMetric(db, 'step_count', 2);
|
|
281
|
+
const activeCal = latestMetric(db, 'active_energy_burned', 2);
|
|
282
|
+
const standHours = latestMetric(db, 'apple_stand_hour', 2);
|
|
283
|
+
|
|
284
|
+
// ── heart health (last 7 days) ─────────────────────────────────────────
|
|
285
|
+
const restingHR = latestMetric(db, 'resting_heart_rate', 7);
|
|
286
|
+
const hrv = latestMetric(db, 'heart_rate_variability_sdnn', 7);
|
|
287
|
+
|
|
288
|
+
// ── recent workouts ────────────────────────────────────────────────────
|
|
289
|
+
const workouts = db.prepare(
|
|
290
|
+
`SELECT date, name, duration_s, calories_kj, avg_hr FROM workouts ORDER BY ts DESC LIMIT 5`
|
|
291
|
+
).all() as { date: string; name: string; duration_s: number | null; calories_kj: number | null; avg_hr: number | null }[];
|
|
292
|
+
|
|
293
|
+
// ── trends ────────────────────────────────────────────────────────────
|
|
294
|
+
const stepTrend = dailyAvgs(db, 'step_count', trendDays);
|
|
295
|
+
const hrTrend = dailyAvgs(db, 'resting_heart_rate', trendDays);
|
|
296
|
+
const hrvTrend = dailyAvgs(db, 'heart_rate_variability_sdnn', trendDays);
|
|
297
|
+
const sleepTrend = sleepDailyAvgs(db, trendDays);
|
|
298
|
+
|
|
299
|
+
// ── vault stats ───────────────────────────────────────────────────────
|
|
300
|
+
const metricsCount = (db.prepare('SELECT COUNT(*) as c FROM metrics').get() as { c: number }).c;
|
|
301
|
+
const sleepCount = (db.prepare('SELECT COUNT(*) as c FROM sleep').get() as { c: number }).c;
|
|
302
|
+
const workoutsCount = (db.prepare('SELECT COUNT(*) as c FROM workouts').get() as { c: number }).c;
|
|
303
|
+
const lastSync = db.prepare('SELECT received_at FROM sync_log ORDER BY received_at DESC LIMIT 1').get() as { received_at: string } | undefined;
|
|
304
|
+
|
|
305
|
+
if (opts.json) {
|
|
306
|
+
console.log(JSON.stringify({
|
|
307
|
+
date: today,
|
|
308
|
+
sleep: sleep ?? null,
|
|
309
|
+
activity: { steps, activeCal, standHours },
|
|
310
|
+
heartHealth: { restingHR, hrv },
|
|
311
|
+
workouts,
|
|
312
|
+
trends: { steps: stepTrend, restingHR: hrTrend, hrv: hrvTrend, sleep: sleepTrend },
|
|
313
|
+
vault: { metricsCount, sleepCount, workoutsCount, lastSync: lastSync?.received_at ?? null }
|
|
314
|
+
}, null, 2));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const lines: string[] = [];
|
|
319
|
+
lines.push(`📅 ${today} | Apple Health Vault`);
|
|
320
|
+
lines.push('');
|
|
321
|
+
|
|
322
|
+
// sleep
|
|
323
|
+
lines.push(ruler('Sleep (last night)'));
|
|
324
|
+
if (sleep) {
|
|
325
|
+
const eff = sleep.in_bed_h && sleep.asleep_h
|
|
326
|
+
? Math.round((sleep.asleep_h / sleep.in_bed_h) * 100) : null;
|
|
327
|
+
lines.push(`😴 ${fmt1(sleep.asleep_h)}h | Efficiency: ${eff != null ? eff + '%' : '—'}`);
|
|
328
|
+
const light = (sleep.asleep_h ?? 0) - (sleep.deep_h ?? 0) - (sleep.rem_h ?? 0);
|
|
329
|
+
const deepPct = sleep.asleep_h ? Math.round(((sleep.deep_h ?? 0) / sleep.asleep_h) * 100) : 0;
|
|
330
|
+
const remPct = sleep.asleep_h ? Math.round(((sleep.rem_h ?? 0) / sleep.asleep_h) * 100) : 0;
|
|
331
|
+
const lightPct = sleep.asleep_h ? Math.round((light / sleep.asleep_h) * 100) : 0;
|
|
332
|
+
lines.push(` Deep: ${fmt1(sleep.deep_h)}h (${deepPct}%) | REM: ${fmt1(sleep.rem_h)}h (${remPct}%) | Light: ${fmt1(light)}h (${lightPct}%)`);
|
|
333
|
+
lines.push(` Awake: ${fmt1(sleep.awake_h)}h | Source: ${sleep.source ?? '—'}`);
|
|
334
|
+
} else {
|
|
335
|
+
lines.push(' No sleep data');
|
|
336
|
+
}
|
|
337
|
+
lines.push('');
|
|
338
|
+
|
|
339
|
+
// activity
|
|
340
|
+
lines.push(ruler('Activity (recent)'));
|
|
341
|
+
const actParts: string[] = [];
|
|
342
|
+
if (steps != null) actParts.push(`👟 ${fmtInt(steps)} steps`);
|
|
343
|
+
if (activeCal != null) actParts.push(`🔥 ${fmtInt(activeCal)} kcal active`);
|
|
344
|
+
if (actParts.length > 0) {
|
|
345
|
+
lines.push(actParts.join(' | '));
|
|
346
|
+
if (standHours != null) lines.push(` Stand hours: ${Math.round(standHours)}`);
|
|
347
|
+
} else {
|
|
348
|
+
lines.push(' No activity data');
|
|
349
|
+
}
|
|
350
|
+
lines.push('');
|
|
351
|
+
|
|
352
|
+
// heart health
|
|
353
|
+
lines.push(ruler('Heart Health'));
|
|
354
|
+
const hh: string[] = [];
|
|
355
|
+
if (restingHR != null) hh.push(`💓 Resting HR: ${Math.round(restingHR)}bpm`);
|
|
356
|
+
if (hrv != null) hh.push(`HRV: ${Math.round(hrv)}ms`);
|
|
357
|
+
lines.push(hh.length > 0 ? hh.join(' | ') : ' No heart data');
|
|
358
|
+
lines.push('');
|
|
359
|
+
|
|
360
|
+
// workouts
|
|
361
|
+
lines.push(ruler('Recent Workouts'));
|
|
362
|
+
if (workouts.length > 0) {
|
|
363
|
+
for (const w of workouts) {
|
|
364
|
+
const emoji = workoutEmoji(w.name);
|
|
365
|
+
const namePadded = pad(w.name, 18);
|
|
366
|
+
const dur = sec2min(w.duration_s);
|
|
367
|
+
const cal = kj2kcal(w.calories_kj);
|
|
368
|
+
lines.push(`${emoji} ${w.date} ${namePadded} ${dur} ${cal}`);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
lines.push(' No workout data');
|
|
372
|
+
}
|
|
373
|
+
lines.push('');
|
|
374
|
+
|
|
375
|
+
// trends
|
|
376
|
+
lines.push(ruler(`${trendDays}-Day Trends`));
|
|
377
|
+
const tl: string[] = [
|
|
378
|
+
trendLine(stepTrend, 'Steps', '', true),
|
|
379
|
+
trendLine(sleepTrend, 'Sleep', 'h'),
|
|
380
|
+
trendLine(hrTrend, 'Resting HR', 'bpm', true),
|
|
381
|
+
trendLine(hrvTrend, 'HRV', 'ms', true),
|
|
382
|
+
].filter(Boolean);
|
|
383
|
+
if (tl.length > 0) lines.push(...tl);
|
|
384
|
+
else lines.push(' Insufficient data for trends');
|
|
385
|
+
lines.push('');
|
|
386
|
+
|
|
387
|
+
// vault stats
|
|
388
|
+
lines.push(ruler('Vault Stats'));
|
|
389
|
+
lines.push(` Metrics: ${metricsCount.toLocaleString()} | Sleep: ${sleepCount} | Workouts: ${workoutsCount}`);
|
|
390
|
+
lines.push(` Last sync: ${lastSync?.received_at ? new Date(lastSync.received_at).toISOString().replace('T', ' ').slice(0, 16) + ' UTC' : 'never'}`);
|
|
391
|
+
|
|
392
|
+
console.log(lines.join('\n'));
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
**Step 2: Register in `src/cli/index.ts`**
|
|
397
|
+
|
|
398
|
+
Add at top (after existing imports):
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { dashboardCommand } from './dashboard.js';
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Add before `program.addCommand(sourcesCommand)`:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
program.addCommand(dashboardCommand);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Step 3: Build**
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
npm run build
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Expected: zero TypeScript errors.
|
|
417
|
+
|
|
418
|
+
**Step 4: Smoke test**
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
npm run dev -- dashboard
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Expected: renders the dashboard with real data from `~/.hae-vault/health.db`. Sections show data or graceful `—` for missing metrics.
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
npm run dev -- dashboard --json
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Expected: valid JSON object.
|
|
431
|
+
|
|
432
|
+
**Step 5: Commit**
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
git add src/cli/dashboard.ts src/cli/index.ts
|
|
436
|
+
git commit -m "feat: add hvault dashboard command"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Task 3: Create `src/cli/trends.ts`
|
|
442
|
+
|
|
443
|
+
**Files:**
|
|
444
|
+
- Create: `src/cli/trends.ts`
|
|
445
|
+
- Modify: `src/cli/index.ts`
|
|
446
|
+
|
|
447
|
+
**Step 1: Create the file**
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
// src/cli/trends.ts
|
|
451
|
+
import { Command } from 'commander';
|
|
452
|
+
import { openDb } from '../db/schema.js';
|
|
453
|
+
|
|
454
|
+
function fmt1(n: number): string {
|
|
455
|
+
return n.toFixed(1);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function fmtInt(n: number): string {
|
|
459
|
+
return Math.round(n).toLocaleString();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function arrow(values: number[]): string {
|
|
463
|
+
if (values.length < 2) return '→';
|
|
464
|
+
const half = Math.floor(values.length / 2);
|
|
465
|
+
const firstHalfAvg = values.slice(0, half).reduce((a, b) => a + b, 0) / half;
|
|
466
|
+
const secondHalfAvg = values.slice(half).reduce((a, b) => a + b, 0) / (values.length - half);
|
|
467
|
+
const delta = secondHalfAvg - firstHalfAvg;
|
|
468
|
+
if (Math.abs(delta) < 0.01 * Math.abs(firstHalfAvg || 1)) return '→';
|
|
469
|
+
return delta > 0 ? '↑' : '↓';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
interface MetricTrendRow { date: string; avg_qty: number }
|
|
473
|
+
|
|
474
|
+
function metricTrend(
|
|
475
|
+
db: ReturnType<typeof openDb>,
|
|
476
|
+
metricName: string,
|
|
477
|
+
since: string
|
|
478
|
+
): { values: number[]; avg: number; min: number; max: number } | null {
|
|
479
|
+
const rows = db.prepare(
|
|
480
|
+
`SELECT date, AVG(qty) as avg_qty FROM metrics
|
|
481
|
+
WHERE metric = ? AND date >= ? AND qty IS NOT NULL
|
|
482
|
+
GROUP BY date ORDER BY date ASC`
|
|
483
|
+
).all(metricName, since) as MetricTrendRow[];
|
|
484
|
+
if (rows.length === 0) return null;
|
|
485
|
+
const values = rows.map(r => r.avg_qty);
|
|
486
|
+
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
487
|
+
return { values, avg, min: Math.min(...values), max: Math.max(...values) };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function sleepTrend(
|
|
491
|
+
db: ReturnType<typeof openDb>,
|
|
492
|
+
since: string
|
|
493
|
+
): { values: number[]; avg: number; min: number; max: number } | null {
|
|
494
|
+
const rows = db.prepare(
|
|
495
|
+
`SELECT asleep_h FROM sleep WHERE date >= ? AND asleep_h IS NOT NULL ORDER BY date ASC`
|
|
496
|
+
).all(since) as { asleep_h: number }[];
|
|
497
|
+
if (rows.length === 0) return null;
|
|
498
|
+
const values = rows.map(r => r.asleep_h);
|
|
499
|
+
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
|
500
|
+
return { values, avg, min: Math.min(...values), max: Math.max(...values) };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export const trendsCommand = new Command('trends')
|
|
504
|
+
.description('Multi-metric trend analysis with averages, ranges, and direction arrows')
|
|
505
|
+
.option('--days <n>', 'Days of history', '7')
|
|
506
|
+
.option('--json', 'Output raw JSON')
|
|
507
|
+
.action((opts) => {
|
|
508
|
+
const db = openDb();
|
|
509
|
+
const days = parseInt(opts.days, 10);
|
|
510
|
+
const since = new Date();
|
|
511
|
+
since.setDate(since.getDate() - days);
|
|
512
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
513
|
+
|
|
514
|
+
const steps = metricTrend(db, 'step_count', sinceStr);
|
|
515
|
+
const restingHR = metricTrend(db, 'resting_heart_rate', sinceStr);
|
|
516
|
+
const hrv = metricTrend(db, 'heart_rate_variability_sdnn', sinceStr);
|
|
517
|
+
const activeCal = metricTrend(db, 'active_energy_burned', sinceStr);
|
|
518
|
+
const sleep = sleepTrend(db, sinceStr);
|
|
519
|
+
|
|
520
|
+
if (opts.json) {
|
|
521
|
+
console.log(JSON.stringify({ days, steps, restingHR, hrv, activeCal, sleep }, null, 2));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const lines: string[] = [];
|
|
526
|
+
lines.push(`📊 ${days}-Day Trends`);
|
|
527
|
+
lines.push('');
|
|
528
|
+
|
|
529
|
+
function row(
|
|
530
|
+
emoji: string,
|
|
531
|
+
label: string,
|
|
532
|
+
data: { values: number[]; avg: number; min: number; max: number } | null,
|
|
533
|
+
unit: string,
|
|
534
|
+
round: boolean
|
|
535
|
+
): void {
|
|
536
|
+
if (!data) return;
|
|
537
|
+
const dir = arrow(data.values);
|
|
538
|
+
const fmt = round ? fmtInt : fmt1;
|
|
539
|
+
lines.push(`${emoji} ${label}: ${fmt(data.avg)} avg (${fmt(data.min)}–${fmt(data.max)}) ${dir}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
row('👟', 'Steps', steps, '', true);
|
|
543
|
+
row('💓', 'Resting HR', restingHR, 'bpm', true);
|
|
544
|
+
row('🧠', 'HRV', hrv, 'ms', true);
|
|
545
|
+
row('😴', 'Sleep', sleep, 'h', false);
|
|
546
|
+
row('🔥', 'Active Cal', activeCal, 'kcal', true);
|
|
547
|
+
|
|
548
|
+
if (lines.length === 2) {
|
|
549
|
+
lines.push(' No trend data available');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
console.log(lines.join('\n'));
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Step 2: Register in `src/cli/index.ts`**
|
|
557
|
+
|
|
558
|
+
Add import:
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { trendsCommand } from './trends.js';
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Add command:
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
program.addCommand(trendsCommand);
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**Step 3: Build**
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
npm run build
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Expected: zero TypeScript errors.
|
|
577
|
+
|
|
578
|
+
**Step 4: Smoke test**
|
|
579
|
+
|
|
580
|
+
```bash
|
|
581
|
+
npm run dev -- trends
|
|
582
|
+
npm run dev -- trends --days 30
|
|
583
|
+
npm run dev -- trends --json
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**Step 5: Commit**
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
git add src/cli/trends.ts src/cli/index.ts
|
|
590
|
+
git commit -m "feat: add hvault trends command"
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Task 4: Enhance `src/cli/summary.ts` with `--color`
|
|
596
|
+
|
|
597
|
+
**Files:**
|
|
598
|
+
- Modify: `src/cli/summary.ts`
|
|
599
|
+
|
|
600
|
+
**Step 1: Rewrite the file**
|
|
601
|
+
|
|
602
|
+
Replace entirely with:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// src/cli/summary.ts
|
|
606
|
+
import { Command } from 'commander';
|
|
607
|
+
import { openDb } from '../db/schema.js';
|
|
608
|
+
|
|
609
|
+
export const summaryCommand = new Command('summary')
|
|
610
|
+
.description('Summarise metrics (averages) over N days')
|
|
611
|
+
.option('--days <n>', 'Last N days', '90')
|
|
612
|
+
.option('--pretty', 'Pretty-print JSON', false)
|
|
613
|
+
.option('-c, --color', 'Pretty terminal output with emoji indicators', false)
|
|
614
|
+
.option('--json', 'Raw JSON (alias for default behavior)', false)
|
|
615
|
+
.action((opts) => {
|
|
616
|
+
const db = openDb();
|
|
617
|
+
const since = new Date();
|
|
618
|
+
since.setDate(since.getDate() - parseInt(opts.days, 10));
|
|
619
|
+
const sinceStr = since.toISOString().slice(0, 10);
|
|
620
|
+
|
|
621
|
+
if (opts.color) {
|
|
622
|
+
// ── colored terminal output ──────────────────────────────────────────
|
|
623
|
+
const days = parseInt(opts.days, 10);
|
|
624
|
+
|
|
625
|
+
function avgMetric(metricName: string): number | null {
|
|
626
|
+
const row = db.prepare(
|
|
627
|
+
`SELECT AVG(qty) as avg FROM metrics WHERE metric = ? AND date >= ? AND qty IS NOT NULL`
|
|
628
|
+
).get(metricName, sinceStr) as { avg: number | null } | undefined;
|
|
629
|
+
return row?.avg ?? null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const steps = avgMetric('step_count');
|
|
633
|
+
const restingHR = avgMetric('resting_heart_rate');
|
|
634
|
+
const hrv = avgMetric('heart_rate_variability_sdnn');
|
|
635
|
+
const activeCal = avgMetric('active_energy_burned');
|
|
636
|
+
|
|
637
|
+
const sleepRow = db.prepare(
|
|
638
|
+
`SELECT AVG(asleep_h) as avg FROM sleep WHERE date >= ? AND asleep_h IS NOT NULL`
|
|
639
|
+
).get(sinceStr) as { avg: number | null } | undefined;
|
|
640
|
+
const sleep = sleepRow?.avg ?? null;
|
|
641
|
+
|
|
642
|
+
const lines: string[] = [`📊 ${days}-Day Summary`, ''];
|
|
643
|
+
if (steps != null) lines.push(`👟 Avg Steps: ${Math.round(steps).toLocaleString()}`);
|
|
644
|
+
if (restingHR != null) lines.push(`💓 Avg Resting HR: ${Math.round(restingHR)}bpm`);
|
|
645
|
+
if (hrv != null) lines.push(`🧠 Avg HRV: ${Math.round(hrv)}ms`);
|
|
646
|
+
if (sleep != null) lines.push(`😴 Avg Sleep: ${sleep.toFixed(1)}h`);
|
|
647
|
+
if (activeCal != null) lines.push(`🔥 Avg Active Cal: ${Math.round(activeCal).toLocaleString()} kcal`);
|
|
648
|
+
|
|
649
|
+
if (lines.length === 2) lines.push(' No summary data available');
|
|
650
|
+
console.log(lines.join('\n'));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── JSON output (default + --pretty) ────────────────────────────────────
|
|
655
|
+
const rows = db.prepare(`
|
|
656
|
+
SELECT metric, units,
|
|
657
|
+
AVG(qty) as avg_qty, MIN(qty) as min_qty, MAX(qty) as max_qty,
|
|
658
|
+
COUNT(*) as count,
|
|
659
|
+
MIN(date) as first_date, MAX(date) as last_date
|
|
660
|
+
FROM metrics
|
|
661
|
+
WHERE date >= ? AND qty IS NOT NULL
|
|
662
|
+
GROUP BY metric, units
|
|
663
|
+
ORDER BY metric ASC
|
|
664
|
+
`).all(sinceStr);
|
|
665
|
+
console.log(opts.pretty ? JSON.stringify(rows, null, 2) : JSON.stringify(rows));
|
|
666
|
+
});
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Step 2: Build**
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
npm run build
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Expected: zero TypeScript errors.
|
|
676
|
+
|
|
677
|
+
**Step 3: Smoke test**
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
npm run dev -- summary --color
|
|
681
|
+
npm run dev -- summary --color --days 30
|
|
682
|
+
npm run dev -- summary --pretty
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Step 4: Commit**
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
git add src/cli/summary.ts
|
|
689
|
+
git commit -m "feat: add --color flag to hvault summary"
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## Task 5: Create `README.md`
|
|
695
|
+
|
|
696
|
+
**Files:**
|
|
697
|
+
- Create: `README.md`
|
|
698
|
+
|
|
699
|
+
**Step 1: Write the file**
|
|
700
|
+
|
|
701
|
+
```markdown
|
|
702
|
+
# hae-vault
|
|
703
|
+
|
|
704
|
+
[](https://www.npmjs.com/package/hae-vault)
|
|
705
|
+
|
|
706
|
+
CLI + HTTP server for Apple Health data from the [Health Auto Export](https://www.healthexportapp.com) iOS app. Ingests via REST API or ZIP file, stores in local SQLite.
|
|
707
|
+
|
|
708
|
+
```bash
|
|
709
|
+
npm install -g hae-vault
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
## Quick start
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
hvault serve # start ingest server (port 4242)
|
|
716
|
+
hvault import export.zip # bulk import from HAE export file
|
|
717
|
+
hvault dashboard # full terminal dashboard
|
|
718
|
+
hvault summary --color # N-day averages with emoji indicators
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
## Setup
|
|
722
|
+
|
|
723
|
+
1. Install the [Health Auto Export](https://www.healthexportapp.com) iOS app
|
|
724
|
+
2. In HAE: Settings → REST API → set server URL to `http://your-server:4242/api/ingest`
|
|
725
|
+
3. Or: export a ZIP from HAE and run `hvault import export.zip`
|
|
726
|
+
|
|
727
|
+
**Optional:** create `.env` in your working directory to override defaults:
|
|
728
|
+
|
|
729
|
+
```env
|
|
730
|
+
HVAULT_DB_PATH=~/.hae-vault/health.db # SQLite DB location
|
|
731
|
+
HVAULT_PORT=4242 # ingest server port
|
|
732
|
+
HVAULT_TOKEN=secret # bearer token for serve (optional)
|
|
733
|
+
HVAULT_WATCH_DIR=~/Downloads # directory to watch for exports
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## Commands
|
|
737
|
+
|
|
738
|
+
### Ingest
|
|
739
|
+
|
|
740
|
+
| Command | Description |
|
|
741
|
+
| --- | --- |
|
|
742
|
+
| `hvault serve` | Start HTTP server, receive HAE REST API pushes |
|
|
743
|
+
| `hvault import <file>` | Import HAE JSON or ZIP export (idempotent) |
|
|
744
|
+
| `hvault watch` | Watch directory and auto-import new HAE exports |
|
|
745
|
+
|
|
746
|
+
### Query
|
|
747
|
+
|
|
748
|
+
Output is JSON by default. Add `--pretty` for formatted JSON.
|
|
749
|
+
|
|
750
|
+
| Command | Description |
|
|
751
|
+
| --- | --- |
|
|
752
|
+
| `hvault metrics --metric <name>` | Time series for a specific metric |
|
|
753
|
+
| `hvault sleep` | Sleep records with stage breakdown |
|
|
754
|
+
| `hvault workouts` | Workout sessions |
|
|
755
|
+
| `hvault summary` | Per-metric averages over N days (JSON) |
|
|
756
|
+
| `hvault query "<sql>"` | Raw SQL query |
|
|
757
|
+
|
|
758
|
+
### Analysis
|
|
759
|
+
|
|
760
|
+
Output is pretty-printed by default. Add `--json` for raw JSON.
|
|
761
|
+
|
|
762
|
+
| Command | Description |
|
|
763
|
+
| --- | --- |
|
|
764
|
+
| `hvault summary --color` | N-day averages with emoji indicators |
|
|
765
|
+
| `hvault dashboard` | Full terminal dashboard with trends |
|
|
766
|
+
| `hvault trends` | Multi-metric trend analysis with direction arrows |
|
|
767
|
+
|
|
768
|
+
### Info
|
|
769
|
+
|
|
770
|
+
| Command | Description |
|
|
771
|
+
| --- | --- |
|
|
772
|
+
| `hvault sources` | Metric coverage in DB (name, count, date range) |
|
|
773
|
+
| `hvault last-sync` | Last HAE REST API push received |
|
|
774
|
+
| `hvault stats` | Row counts per table |
|
|
775
|
+
|
|
776
|
+
## Example output
|
|
777
|
+
|
|
778
|
+
`hvault dashboard`:
|
|
779
|
+
```
|
|
780
|
+
📅 2026-02-18 | Apple Health Vault
|
|
781
|
+
|
|
782
|
+
── Sleep (last night) ────────────────
|
|
783
|
+
😴 7.2h | Efficiency: 94%
|
|
784
|
+
Deep: 1.5h (21%) | REM: 2.1h (29%) | Light: 3.6h (50%)
|
|
785
|
+
Awake: 0.3h | Source: Apple Watch
|
|
786
|
+
|
|
787
|
+
── Activity (recent) ─────────────────
|
|
788
|
+
👟 8,432 steps | 🔥 420 kcal active
|
|
789
|
+
Stand hours: 10
|
|
790
|
+
|
|
791
|
+
── Heart Health ──────────────────────
|
|
792
|
+
💓 Resting HR: 58bpm | HRV: 44ms
|
|
793
|
+
|
|
794
|
+
── Recent Workouts ───────────────────
|
|
795
|
+
🚶 2026-02-17 Walking 45min 280 kcal
|
|
796
|
+
🚴 2026-02-15 Cycling 62min 420 kcal
|
|
797
|
+
|
|
798
|
+
── 7-Day Trends ──────────────────────
|
|
799
|
+
Steps: 7,200 → 8,432 ↑ (avg 7,840)
|
|
800
|
+
Sleep: 6.8h → 7.2h ↑ (avg 7.1h)
|
|
801
|
+
Resting HR: 60 → 58bpm ↓ (avg 59)
|
|
802
|
+
HRV: 42 → 44ms ↑ (avg 43)
|
|
803
|
+
|
|
804
|
+
── Vault Stats ───────────────────────
|
|
805
|
+
Metrics: 570,432 | Sleep: 365 | Workouts: 248
|
|
806
|
+
Last sync: 2026-02-18 09:23 UTC
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
`hvault summary --color --days 30`:
|
|
810
|
+
```
|
|
811
|
+
📊 30-Day Summary
|
|
812
|
+
|
|
813
|
+
👟 Avg Steps: 8,432
|
|
814
|
+
💓 Avg Resting HR: 58bpm
|
|
815
|
+
🧠 Avg HRV: 44ms
|
|
816
|
+
😴 Avg Sleep: 7.2h
|
|
817
|
+
🔥 Avg Active Cal: 420 kcal
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
`hvault trends --days 7`:
|
|
821
|
+
```
|
|
822
|
+
📊 7-Day Trends
|
|
823
|
+
|
|
824
|
+
👟 Steps: 8,432 avg (6,100–11,200) ↑
|
|
825
|
+
💓 Resting HR: 58bpm avg (55–62) ↓
|
|
826
|
+
🧠 HRV: 44ms avg (38–52) ↑
|
|
827
|
+
😴 Sleep: 7.2h avg (5.5–8.9h) ↑
|
|
828
|
+
🔥 Active Cal: 420 kcal avg (280–620) →
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
`hvault stats`:
|
|
832
|
+
```json
|
|
833
|
+
{"metrics":570432,"sleep":365,"workouts":248,"syncs":12}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
## Flags
|
|
837
|
+
|
|
838
|
+
### Ingest flags
|
|
839
|
+
|
|
840
|
+
| Flag | Applies to | Description |
|
|
841
|
+
| --- | --- | --- |
|
|
842
|
+
| `--port <n>` | serve | HTTP port (default: 4242) |
|
|
843
|
+
| `--token <secret>` | serve | Require `Authorization: Bearer` header |
|
|
844
|
+
| `--target <name>` | import, watch | Tag data with device/person name |
|
|
845
|
+
| `--dir <path>` | watch | Directory to watch |
|
|
846
|
+
| `--interval <s>` | watch | Poll interval in seconds (default: 60) |
|
|
847
|
+
|
|
848
|
+
### Query flags
|
|
849
|
+
|
|
850
|
+
| Flag | Description |
|
|
851
|
+
| --- | --- |
|
|
852
|
+
| `--days <n>` | Days of history (default varies per command) |
|
|
853
|
+
| `--metric <name>` | Metric name, e.g. `step_count`, `heart_rate` |
|
|
854
|
+
| `--pretty` | Pretty-print JSON output |
|
|
855
|
+
|
|
856
|
+
### Analysis flags
|
|
857
|
+
|
|
858
|
+
| Flag | Applies to | Description |
|
|
859
|
+
| --- | --- | --- |
|
|
860
|
+
| `--days <n>` | all analysis | Days of history (default: 7 or 90) |
|
|
861
|
+
| `-c, --color` | summary | Pretty terminal output |
|
|
862
|
+
| `--json` | dashboard, trends | Raw JSON output |
|
|
863
|
+
|
|
864
|
+
## Environment variables
|
|
865
|
+
|
|
866
|
+
Load order: CLI flag > env var > `.env` file > default.
|
|
867
|
+
|
|
868
|
+
```bash
|
|
869
|
+
HVAULT_DB_PATH=~/.hae-vault/health.db # SQLite DB location
|
|
870
|
+
HVAULT_PORT=4242 # serve port
|
|
871
|
+
HVAULT_TOKEN=secret # bearer token for serve
|
|
872
|
+
HVAULT_WATCH_DIR=~/Downloads # directory to watch
|
|
873
|
+
HVAULT_WATCH_INTERVAL=60 # watch poll interval (seconds)
|
|
874
|
+
HVAULT_TARGET=default # default target name
|
|
875
|
+
HVAULT_ENV_FILE=/path/to/.env # override .env file location
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
## Exit codes
|
|
879
|
+
|
|
880
|
+
| Code | Meaning |
|
|
881
|
+
| --- | --- |
|
|
882
|
+
| 0 | Success |
|
|
883
|
+
| 1 | General error |
|
|
884
|
+
|
|
885
|
+
## Development
|
|
886
|
+
|
|
887
|
+
```bash
|
|
888
|
+
git clone https://github.com/mrkhachaturov/hae-vault.git
|
|
889
|
+
cd hae-vault
|
|
890
|
+
npm install
|
|
891
|
+
|
|
892
|
+
npm run dev -- serve # run server without building
|
|
893
|
+
npm run dev -- dashboard # run dashboard without building
|
|
894
|
+
npm run build # compile TypeScript → dist/
|
|
895
|
+
npm test # run test suite
|
|
896
|
+
npm install -g . # install globally as hvault
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Node.js 22+ required.
|
|
900
|
+
|
|
901
|
+
## Full command reference
|
|
902
|
+
|
|
903
|
+
→ [docs/COMMANDS.md](docs/COMMANDS.md)
|
|
904
|
+
|
|
905
|
+
## License
|
|
906
|
+
|
|
907
|
+
MIT
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Step 2: Commit**
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
git add README.md
|
|
914
|
+
git commit -m "docs: add README.md"
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## Task 6: Create `docs/COMMANDS.md`
|
|
920
|
+
|
|
921
|
+
**Files:**
|
|
922
|
+
- Create: `docs/COMMANDS.md`
|
|
923
|
+
|
|
924
|
+
**Step 1: Write the file**
|
|
925
|
+
|
|
926
|
+
Write a detailed reference following the whoop-sync/docs/COMMANDS.md pattern. Groups: Ingest, Query, Analysis, Info. Each command gets: description, flags table, example invocation, example output.
|
|
927
|
+
|
|
928
|
+
Full content:
|
|
929
|
+
|
|
930
|
+
```markdown
|
|
931
|
+
# hae-vault — Full Command Reference
|
|
932
|
+
|
|
933
|
+
← [Back to README](../README.md)
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## Ingest commands
|
|
938
|
+
|
|
939
|
+
### `serve`
|
|
940
|
+
|
|
941
|
+
Start an HTTP server that receives Health Auto Export REST API pushes.
|
|
942
|
+
|
|
943
|
+
```bash
|
|
944
|
+
hvault serve
|
|
945
|
+
hvault serve --port 4242 --token mysecret
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
| Flag | Description |
|
|
949
|
+
| --- | --- |
|
|
950
|
+
| `--port <n>` | HTTP port (default: 4242) |
|
|
951
|
+
| `--token <secret>` | Require `Authorization: Bearer <secret>` header |
|
|
952
|
+
|
|
953
|
+
Configure HAE: Settings → REST API → URL: `http://your-server:4242/api/ingest`
|
|
954
|
+
|
|
955
|
+
---
|
|
956
|
+
|
|
957
|
+
### `import`
|
|
958
|
+
|
|
959
|
+
Bulk import from a HAE JSON or ZIP export file. Idempotent — skips already-imported files via SHA-256 hash.
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
hvault import export.json
|
|
963
|
+
hvault import export.zip
|
|
964
|
+
hvault import export.zip --target me
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
| Flag | Description |
|
|
968
|
+
| --- | --- |
|
|
969
|
+
| `--target <name>` | Tag imported data with device/person name |
|
|
970
|
+
|
|
971
|
+
Output:
|
|
972
|
+
```json
|
|
973
|
+
{"metrics":1234,"sleep":7,"workouts":3,"skipped":false}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
### `watch`
|
|
979
|
+
|
|
980
|
+
Poll a directory for new HAE export files and auto-import on schedule.
|
|
981
|
+
|
|
982
|
+
```bash
|
|
983
|
+
hvault watch
|
|
984
|
+
hvault watch --dir ~/Downloads
|
|
985
|
+
hvault watch --dir ~/Downloads --interval 60
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
| Flag | Description |
|
|
989
|
+
| --- | --- |
|
|
990
|
+
| `--dir <path>` | Directory to watch (default: `HVAULT_WATCH_DIR`) |
|
|
991
|
+
| `--interval <s>` | Poll interval in seconds (default: 60) |
|
|
992
|
+
| `--target <name>` | Tag imported data with device/person name |
|
|
993
|
+
|
|
994
|
+
---
|
|
995
|
+
|
|
996
|
+
## Query commands
|
|
997
|
+
|
|
998
|
+
All query commands output JSON by default. Add `--pretty` for formatted JSON.
|
|
999
|
+
|
|
1000
|
+
### Shared flags
|
|
1001
|
+
|
|
1002
|
+
| Flag | Description |
|
|
1003
|
+
| --- | --- |
|
|
1004
|
+
| `--days <n>` | Days of history |
|
|
1005
|
+
| `--pretty` | Pretty-print JSON output |
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
### `metrics`
|
|
1010
|
+
|
|
1011
|
+
Time series for a specific health metric.
|
|
1012
|
+
|
|
1013
|
+
```bash
|
|
1014
|
+
hvault metrics --metric step_count --days 30
|
|
1015
|
+
hvault metrics --metric heart_rate --days 7 --pretty
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
| Flag | Description |
|
|
1019
|
+
| --- | --- |
|
|
1020
|
+
| `--metric <name>` | **Required.** Metric name (e.g. `step_count`, `resting_heart_rate`) |
|
|
1021
|
+
| `--days <n>` | Last N days (default: 30) |
|
|
1022
|
+
|
|
1023
|
+
Output (default JSON):
|
|
1024
|
+
```json
|
|
1025
|
+
[{"ts":"2026-02-18T00:00:00Z","date":"2026-02-18","qty":8432,"min":null,"avg":null,"max":null,"units":"count","source":"iPhone","target":"default"}]
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
To discover available metric names: `hvault sources`
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
### `sleep`
|
|
1033
|
+
|
|
1034
|
+
Sleep records with stage breakdown.
|
|
1035
|
+
|
|
1036
|
+
```bash
|
|
1037
|
+
hvault sleep --days 14
|
|
1038
|
+
hvault sleep --days 7 --pretty
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Output:
|
|
1042
|
+
```json
|
|
1043
|
+
[{"date":"2026-02-17","sleep_start":"2026-02-17T22:45:00Z","sleep_end":"2026-02-18T06:00:00Z","core_h":null,"deep_h":1.5,"rem_h":2.1,"awake_h":0.3,"asleep_h":7.2,"in_bed_h":7.5,"schema_ver":"aggregated_v2","source":"Apple Watch"}]
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
### `workouts`
|
|
1049
|
+
|
|
1050
|
+
Workout sessions with duration, calories, and heart rate.
|
|
1051
|
+
|
|
1052
|
+
```bash
|
|
1053
|
+
hvault workouts --days 30
|
|
1054
|
+
hvault workouts --days 7 --pretty
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
Output:
|
|
1058
|
+
```json
|
|
1059
|
+
[{"ts":"2026-02-17T18:30:00Z","date":"2026-02-17","name":"Walking","duration_s":2700,"calories_kj":1172,"distance":3.2,"distance_unit":"km","avg_hr":98,"max_hr":121,"target":"default"}]
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Note: `calories_kj` is kilojoules. Divide by 4.184 for kcal.
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
### `summary`
|
|
1067
|
+
|
|
1068
|
+
Per-metric averages over N days. JSON output by default.
|
|
1069
|
+
|
|
1070
|
+
```bash
|
|
1071
|
+
hvault summary --days 90
|
|
1072
|
+
hvault summary --days 30 --pretty
|
|
1073
|
+
hvault summary --color --days 30 # pretty terminal output
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
JSON output:
|
|
1077
|
+
```json
|
|
1078
|
+
[{"metric":"step_count","units":"count","avg_qty":8432,"min_qty":3200,"max_qty":14100,"count":87,"first_date":"2025-11-20","last_date":"2026-02-18"}]
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
With `--color`:
|
|
1082
|
+
```
|
|
1083
|
+
📊 30-Day Summary
|
|
1084
|
+
|
|
1085
|
+
👟 Avg Steps: 8,432
|
|
1086
|
+
💓 Avg Resting HR: 58bpm
|
|
1087
|
+
🧠 Avg HRV: 44ms
|
|
1088
|
+
😴 Avg Sleep: 7.2h
|
|
1089
|
+
🔥 Avg Active Cal: 420 kcal
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
### `query`
|
|
1095
|
+
|
|
1096
|
+
Raw SQL query against the SQLite database.
|
|
1097
|
+
|
|
1098
|
+
```bash
|
|
1099
|
+
hvault query "SELECT date, AVG(qty) FROM metrics WHERE metric='step_count' GROUP BY date ORDER BY date DESC LIMIT 7"
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
Returns JSON array of row objects. Use `--pretty` for formatted output.
|
|
1103
|
+
|
|
1104
|
+
**Tables:** `metrics`, `sleep`, `workouts`, `sync_log`, `import_log`
|
|
1105
|
+
|
|
1106
|
+
---
|
|
1107
|
+
|
|
1108
|
+
## Analysis commands
|
|
1109
|
+
|
|
1110
|
+
### `dashboard`
|
|
1111
|
+
|
|
1112
|
+
Full terminal dashboard: sleep, activity, heart health, recent workouts, N-day trends, vault stats.
|
|
1113
|
+
|
|
1114
|
+
```bash
|
|
1115
|
+
hvault dashboard
|
|
1116
|
+
hvault dashboard --days 14
|
|
1117
|
+
hvault dashboard --json
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
| Flag | Description |
|
|
1121
|
+
| --- | --- |
|
|
1122
|
+
| `--days <n>` | Trend window in days (default: 7) |
|
|
1123
|
+
| `--json` | Output raw JSON (for AI agent use) |
|
|
1124
|
+
|
|
1125
|
+
Output:
|
|
1126
|
+
```
|
|
1127
|
+
📅 2026-02-18 | Apple Health Vault
|
|
1128
|
+
|
|
1129
|
+
── Sleep (last night) ────────────────
|
|
1130
|
+
😴 7.2h | Efficiency: 94%
|
|
1131
|
+
Deep: 1.5h (21%) | REM: 2.1h (29%) | Light: 3.6h (50%)
|
|
1132
|
+
Awake: 0.3h | Source: Apple Watch
|
|
1133
|
+
|
|
1134
|
+
── Activity (recent) ─────────────────
|
|
1135
|
+
👟 8,432 steps | 🔥 420 kcal active
|
|
1136
|
+
Stand hours: 10
|
|
1137
|
+
|
|
1138
|
+
── Heart Health ──────────────────────
|
|
1139
|
+
💓 Resting HR: 58bpm | HRV: 44ms
|
|
1140
|
+
|
|
1141
|
+
── Recent Workouts ───────────────────
|
|
1142
|
+
🚶 2026-02-17 Walking 45min 280 kcal
|
|
1143
|
+
🚴 2026-02-15 Cycling 62min 420 kcal
|
|
1144
|
+
|
|
1145
|
+
── 7-Day Trends ──────────────────────
|
|
1146
|
+
Steps: 7,200 → 8,432 ↑ (avg 7,840)
|
|
1147
|
+
Sleep: 6.8h → 7.2h ↑ (avg 7.1h)
|
|
1148
|
+
Resting HR: 60 → 58bpm ↓ (avg 59)
|
|
1149
|
+
HRV: 42 → 44ms ↑ (avg 43)
|
|
1150
|
+
|
|
1151
|
+
── Vault Stats ───────────────────────
|
|
1152
|
+
Metrics: 570,432 | Sleep: 365 | Workouts: 248
|
|
1153
|
+
Last sync: 2026-02-18 09:23 UTC
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
### `trends`
|
|
1159
|
+
|
|
1160
|
+
Multi-metric trend analysis with averages, ranges, and direction arrows.
|
|
1161
|
+
|
|
1162
|
+
```bash
|
|
1163
|
+
hvault trends
|
|
1164
|
+
hvault trends --days 30
|
|
1165
|
+
hvault trends --json
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
| Flag | Description |
|
|
1169
|
+
| --- | --- |
|
|
1170
|
+
| `--days <n>` | Days of history (default: 7) |
|
|
1171
|
+
| `--json` | Raw JSON output |
|
|
1172
|
+
|
|
1173
|
+
Output:
|
|
1174
|
+
```
|
|
1175
|
+
📊 7-Day Trends
|
|
1176
|
+
|
|
1177
|
+
👟 Steps: 8,432 avg (6,100–11,200) ↑
|
|
1178
|
+
💓 Resting HR: 58bpm avg (55–62) ↓
|
|
1179
|
+
🧠 HRV: 44ms avg (38–52) ↑
|
|
1180
|
+
😴 Sleep: 7.2h avg (5.5–8.9h) ↑
|
|
1181
|
+
🔥 Active Cal: 420 kcal avg (280–620) →
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
Direction logic: compares first-half average vs second-half average of the period.
|
|
1185
|
+
|
|
1186
|
+
---
|
|
1187
|
+
|
|
1188
|
+
## Info commands
|
|
1189
|
+
|
|
1190
|
+
### `sources`
|
|
1191
|
+
|
|
1192
|
+
Show which metrics are in the DB and their date coverage.
|
|
1193
|
+
|
|
1194
|
+
```bash
|
|
1195
|
+
hvault sources
|
|
1196
|
+
hvault sources --pretty
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
Output:
|
|
1200
|
+
```json
|
|
1201
|
+
[{"metric":"active_energy_burned","units":"kcal","count":365,"first_date":"2025-02-18","last_date":"2026-02-18"}]
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
---
|
|
1205
|
+
|
|
1206
|
+
### `last-sync`
|
|
1207
|
+
|
|
1208
|
+
Show when the last HAE REST API push was received.
|
|
1209
|
+
|
|
1210
|
+
```bash
|
|
1211
|
+
hvault last-sync
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
Output:
|
|
1215
|
+
```json
|
|
1216
|
+
{"id":42,"received_at":"2026-02-18T09:23:11.000Z","target":"default","metrics_count":247,"workouts_count":0}
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
Returns `null` if no pushes have been received.
|
|
1220
|
+
|
|
1221
|
+
---
|
|
1222
|
+
|
|
1223
|
+
### `stats`
|
|
1224
|
+
|
|
1225
|
+
Row counts per table.
|
|
1226
|
+
|
|
1227
|
+
```bash
|
|
1228
|
+
hvault stats
|
|
1229
|
+
hvault stats --pretty
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
Output:
|
|
1233
|
+
```json
|
|
1234
|
+
{"metrics":570432,"sleep":365,"workouts":248,"syncs":12}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
---
|
|
1238
|
+
|
|
1239
|
+
← [Back to README](../README.md)
|
|
1240
|
+
```
|
|
1241
|
+
|
|
1242
|
+
**Step 2: Commit**
|
|
1243
|
+
|
|
1244
|
+
```bash
|
|
1245
|
+
git add docs/COMMANDS.md
|
|
1246
|
+
git commit -m "docs: add docs/COMMANDS.md full command reference"
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## Task 7: Git init and push to GitHub
|
|
1252
|
+
|
|
1253
|
+
**Files:** none
|
|
1254
|
+
|
|
1255
|
+
**Step 1: Initialize git and set up**
|
|
1256
|
+
|
|
1257
|
+
> Note: The repo already has a `.git` directory. Skip `git init` if `git status` works.
|
|
1258
|
+
|
|
1259
|
+
```bash
|
|
1260
|
+
cd /Volumes/storage/01_Projects/whoop/hae-vault && git status
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
If "not a git repository": run `git init && git branch -M main`
|
|
1264
|
+
|
|
1265
|
+
**Step 2: Check current git state**
|
|
1266
|
+
|
|
1267
|
+
```bash
|
|
1268
|
+
git log --oneline -5
|
|
1269
|
+
git status
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
**Step 3: Add remote (if not already set)**
|
|
1273
|
+
|
|
1274
|
+
```bash
|
|
1275
|
+
git remote -v
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
If no remote: `git remote add origin git@github.com:mrkhachaturov/hae-vault.git`
|
|
1279
|
+
|
|
1280
|
+
**Step 4: Push to main**
|
|
1281
|
+
|
|
1282
|
+
```bash
|
|
1283
|
+
git push -u origin main
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
Expected: branch pushed, tracking set.
|
|
1287
|
+
|
|
1288
|
+
---
|
|
1289
|
+
|
|
1290
|
+
## Final verification
|
|
1291
|
+
|
|
1292
|
+
```bash
|
|
1293
|
+
# Build is clean
|
|
1294
|
+
npm run build
|
|
1295
|
+
|
|
1296
|
+
# All tests pass
|
|
1297
|
+
npm test
|
|
1298
|
+
|
|
1299
|
+
# CLI help works
|
|
1300
|
+
npm run dev -- --help
|
|
1301
|
+
npm run dev -- dashboard --help
|
|
1302
|
+
npm run dev -- trends --help
|
|
1303
|
+
npm run dev -- summary --help
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
Expected: zero build errors, all tests pass, help text shows new commands.
|