chore(scripts): drop private host names from the demo tooling

The demo migration was named after the host it targets, and its
README, the v1.5 demo seed and a contributing note named the
maintainer's servers, a container id, an app id and two account ids.
The script already takes every host, container and account from its
environment, so the prose now says source and target and lists the
variables instead. It is renamed to migrate-demo-account.sh; the
password guard follows the new name.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:27:22 +01:00
1 parent 9620b0116a
commit 3ba5efb025
6 files changed
+124 -123

No files matched your search

+1 -1
View File
@@ -185,7 +185,7 @@ If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`):
### Long-term
Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback. Tracking issue: `v1.6 — Weblate self-hosted on edge-01`.
Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback.
## Code of Conduct
+1 -1
View File
@@ -37,7 +37,7 @@ typecheck.
- **`repair-google-health-sleep.ts`**: re-syncs every Google Health connection so nights stored twice before v1.28.18 collapse to one set; dry-run by default, `--run` to execute.
- **`i18n-key-tool.mjs`**, **`split-description.mjs`**: edit one key across every `messages/*.json` (show, set, drop) and move an over-long card description's tail into its own key; **`measure-description-lengths.mjs`** measures the Settings and Admin description strings against the 120-character guideline.
- **`audit-column-readers.ts`**: lists write-only schema columns and open `@pending-consumer` markers; a review tool, not a gate.
- **`migrate-demo-edge01.sh`**: copies the demo account from one instance to a separate demo instance; see its README.
- **`migrate-demo-account.sh`**: copies the demo account from one instance to a separate demo instance; see its README.
- **`__tests__/`**: tests for the scripts above.
## Conventions
@@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest";
* demo.healthlog.dev. Its Argon2id hash lives in two places that must agree:
*
* - scripts/seed-demo.ts writes it when a demo tenant is seeded
* - scripts/migrate-demo-edge01.sh overwrites it after a migration load
* - scripts/migrate-demo-account.sh overwrites it after a migration load
*
* Public issue #805: the migration used to carry the SOURCE account's hash
* across instead, so once the operator rotated their own demo password the
@@ -18,7 +18,7 @@ import { describe, expect, it } from "vitest";
const REPO_ROOT = join(__dirname, "..", "..");
const SEED = join(REPO_ROOT, "scripts", "seed-demo.ts");
const MIGRATE = join(REPO_ROOT, "scripts", "migrate-demo-edge01.sh");
const MIGRATE = join(REPO_ROOT, "scripts", "migrate-demo-account.sh");
/** Every Argon2id hash literal in a file, in source order. */
function argon2Hashes(path: string): string[] {
@@ -27,7 +27,7 @@ function argon2Hashes(path: string): string[] {
}
describe("published demo password", () => {
it("is the same hash in the seed script and the edge01 migration", () => {
it("is the same hash in the seed script and the demo migration", () => {
const seeded = argon2Hashes(SEED);
const migrated = argon2Hashes(MIGRATE);
@@ -55,7 +55,7 @@ describe("published demo password", () => {
// ...and it must be proven, not merely attempted. Without the check a
// mangled hash would reach a visitor's login screen instead of the script.
expect(script).toContain(
"ABORT/FAIL: demo password_hash on edge01 is not the published literal",
"ABORT/FAIL: demo password_hash on the target is not the published literal",
);
});
@@ -1,28 +1,30 @@
# migrate-demo-edge01.sh
# migrate-demo-account.sh
Copies **exactly one** user — the demo account
`usr_demo_cf31025295714ece8d91f5af13afd76d` — from the production apps01
Postgres to the **separate** edge01 demo Postgres. No other user's rows are
transferred under any circumstance.
Copies **exactly one** user, the demo account named by `DEMO_USER_ID`, from a
source instance's Postgres to the **separate** Postgres of a demo instance. No
other user's rows are transferred under any circumstance.
> Review the script first, then run it deliberately. It performs destructive,
> demo-scoped deletes on the edge01 demo DB, then loads the apps01 demo slice.
> It never writes to apps01 (source is read-only).
> demo-scoped deletes on the target demo DB, then loads the demo slice from the
> source. It never writes to the source (read-only).
## How it connects
- **Source (apps01):** SSH alias `apps-01`, container `db-pg8wggwogo8c4gc4ks0kk4ss`
(stable), psql `healthlog`/`healthlog`.
- **Target (edge01):** SSH alias `edge-01`. The demo Postgres is a separate
Coolify resource whose container name is **not hardcoded** — the script
resolves it at runtime via `docker ps | grep -iE 'postgres|^db-'` and aborts
if it cannot pick a single match (override with `DST_DB_CONTAINER=db-xxxx`).
The demo **app** uuid is `ck8cs4osswg8w440gskw08w8`; match its linked
Postgres resource, not the docs/landing DB.
- **Edge demo user id is resolved at runtime** by `username='demo'` (it may
differ from the apps01 id). The wipe phase is scoped to that resolved id; the
load then brings the apps01 row in, so afterwards the edge01 demo user
carries the apps01 canonical id.
Everything instance-specific comes from the environment:
- `DEMO_USER_ID`: the user id to copy. `PROTECT_USER_ID`: an id that must
never appear on the target (the operator's own account).
- `SRC_SSH` and `SRC_DB_CONTAINER_PATTERN`: the source host's ssh alias and a
`grep -E` pattern for its database container; psql `healthlog`/`healthlog`.
- `DST_SSH`: the target host's ssh alias. The demo Postgres container is
**not hardcoded**: the script resolves it at runtime via
`docker ps | grep -iE 'postgres|^db-'` and aborts if it cannot pick a single
match (override with `DST_DB_CONTAINER=db-xxxx`). Match the demo app's own
Postgres, not a docs or landing-page database on the same host.
- **The target demo user id is resolved at runtime** by `username='demo'` (it
may differ from the source id). The wipe phase is scoped to that resolved id;
the load then brings the source row in, so afterwards the target demo user
carries the source's canonical id.
Each table is transferred with a scoped, server-side `\copy ... TO STDOUT`
piped host-to-host into `\copy ... FROM STDIN`, using **explicit column lists
@@ -97,9 +99,9 @@ acknowledgement).
`*_oauth_states` / `*_connect_tickets`, telegram scheduled-deletions +
reminder messages, clinician_share_links, provider_health) — tied to live
credentials.
- **Encrypted-at-rest payloads we can't decrypt on edge01:**
- **Encrypted-at-rest payloads the target cannot decrypt:**
`insight_narratives` and all `coach_*` (coach_conversations, coach_messages,
coach_facts, coach_usage). The edge01 instance does not hold the prod
coach_facts, coach_usage). The target does not hold the source's
encryption key, so the AES-256-GCM ciphertext would be unreadable. The same
reason drives NULLing `cycle_day_logs.sensitive_encrypted` /
`notes_encrypted` while still carrying that table's plaintext cycle fields.
@@ -109,16 +111,16 @@ acknowledgement).
import_jobs, host_metrics, feedback, recommendation_feedback, and all rollup
tables (measurement_rollups, mood_entry_rollups, strain_trimp_cache,
cycle_predictions) — these
rebuild on edge01.
rebuild on the target.
- **No rows:** workouts / workout_routes / workout_samples (demo has none).
- **Instance / global:** app_settings; global catalogues (mood_tags,
cycle_symptoms) — these pre-exist on edge01 and the carried link rows
cycle_symptoms) pre-exist on the target and the carried link rows
reference the global ids only.
## Encryption note
The edge01 demo instance is assumed **not** to share the apps01 encryption
key. Every column whose value is AES-256-GCM ciphertext under the prod key is
The demo instance is assumed **not** to share the source's encryption key.
Every column whose value is AES-256-GCM ciphertext under the source key is
either NULLed (secrets on `users`, `cycle_day_logs.*_encrypted`) or skipped
entirely (`insight_narratives`, `coach_messages.encryptedContent`). Carrying
that ciphertext would produce undecryptable rows that the app's fail-closed
@@ -128,38 +130,37 @@ loader would reject.
The demo user is loaded with `role='ADMIN'` so the admin surfaces are
viewable in the demo. This is **only safe because DEMO_MODE must stay ON** on
edge01 — the proxy layer (`src/proxy.ts`) blocks every mutating request in
the target: the proxy layer (`src/proxy.ts`) blocks every mutating request in
demo mode, so an admin demo session can browse admin pages but cannot change
anything. If DEMO_MODE were ever off on edge01, this admin demo account would
anything. If DEMO_MODE were ever off on the target, this admin demo account would
be a write-capable admin. Keep DEMO_MODE enabled.
## Safety mechanisms built into the script
- `set -euo pipefail` and `ON_ERROR_STOP=1` on every psql call.
- **Pre-flight guard (apps01):** for each carried set, a query that must
- **Pre-flight guard (source):** for each carried set, a query that must
return `0` foreign-owned rows — aborts on any non-zero. Includes a check
that the demo owns **0 custom mood_tags** (so every carried
`mood_entry_tag_link` points at a global tag that pre-exists on edge01) plus
`mood_entry_tag_link` points at a global tag that pre-exists on the target) plus
a defensive "non-global tag" link count.
- **Scoped wipe-first (edge01):** FK-child-first deletes, every one scoped to
the runtime-resolved edge01 demo id. No unscoped delete anywhere. Refuses to
proceed if `username='demo'` on edge01 resolves to the operator id.
- **Post-load verification (edge01):** per-table demo row counts; a hard
assertion that the operator id `cmlupy4tn000001rpzx1pxvz7` appears in zero
rows; a check that user_id-bearing carried tables hold only the demo id; and
- **Scoped wipe-first (target):** FK-child-first deletes, every one scoped to
the runtime-resolved target demo id. No unscoped delete anywhere. Refuses to
proceed if `username='demo'` on the target resolves to the operator id.
- **Post-load verification (target):** per-table demo row counts; a hard
assertion that `PROTECT_USER_ID` appears in zero rows; a check that user_id-bearing carried tables hold only the demo id; and
a confirmation the demo user exists with `role=ADMIN`.
## Caveats / things to confirm before running
- **`FOREIGN_HITS` assertion** counts _all_ non-demo rows in the carried
tables on edge01, not just what this run inserted. On a clean
single-demo-user DB it is `0`. If edge01 legitimately hosts other demo-only
tables on the target, not just what this run inserted. On a clean
single-demo-user DB it is `0`. If the target legitimately hosts other demo-only
accounts, treat a non-zero value as a warning to inspect, not an automatic
failure (the script prints it as a WARNING, not an abort).
- Confirm edge01 psql credentials are `healthlog`/`healthlog` (the script
- Confirm the target's psql credentials are `healthlog`/`healthlog` (the script
verifies connectivity before any write; adjust `PG_USER` / `PG_DB` if the
demo Postgres resource uses different ones).
- The script assumes edge01's schema is at the same (or a compatible) Prisma
migration level as apps01. A column present on apps01 but missing on edge01
fails the `\copy` loudly rather than silently corrupting data — run
migrations on edge01 first if the two have drifted.
- The script assumes the target's schema is at the same (or a compatible)
Prisma migration level as the source. A column present on the source but
missing on the target fails the `\copy` loudly rather than silently
corrupting data; run migrations on the target first if the two have drifted.
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
#
# migrate-demo-edge01.sh — copy EXACTLY ONE user (the demo account) from the
# production apps01 Postgres to the SEPARATE edge01 demo Postgres.
# migrate-demo-account.sh: copy EXACTLY ONE user (the demo account) from the
# source Postgres to the SEPARATE target demo Postgres.
#
# ============================================================================
# WHAT THIS SCRIPT DOES (and the isolation guarantees it makes)
# ============================================================================
#
# It transfers a single, demo-scoped slice of health data so the public demo
# instance on edge01 shows a realistic-looking account WITHOUT carrying any
# instance on the target shows a realistic-looking account WITHOUT carrying any
# real-operator data. The ONLY user copied is:
#
# $DEMO_USER_ID (the single demo account, supplied via env)
@@ -26,7 +26,7 @@
# 3. No secret / credential column is transferred. The users SELECT NULLs
# every encrypted / token / integration-secret column explicitly. The
# cycle-day-log SELECT NULLs the two app-encrypted columns (they are
# keyed to the prod encryption key, which is NOT present on edge01).
# keyed to the prod encryption key, which is NOT present on the target).
#
# The transfer mechanism per table is a scoped server-side COPY piped
# host-to-host:
@@ -42,12 +42,12 @@
# - an added/dropped column on either side fails loudly rather than
# silently shifting data into the wrong column.
#
# Tables load in FK-parent-first order. The edge01 demo rows are wiped first
# (FK-child-first), scoped to the edge01 demo user id resolved at runtime.
# Tables load in FK-parent-first order. The target demo rows are wiped first
# (FK-child-first), scoped to the target demo user id resolved at runtime.
#
# THIS SCRIPT IS FOR REVIEW. Read it, then run it deliberately. It performs
# DESTRUCTIVE deletes on the edge01 demo DB (scoped to the demo user) followed
# by inserts. It NEVER writes to apps01 (source is read-only).
# DESTRUCTIVE deletes on the target demo DB (scoped to the demo user) followed
# by inserts. It NEVER writes to the source (source is read-only).
#
# ----------------------------------------------------------------------------
# EXCLUDED ENTIRELY (intentionally NOT copied)
@@ -59,16 +59,16 @@
# fitbit_connections, all *_oauth_states / *_connect_tickets,
# telegram_scheduled_deletions, telegram_reminder_messages,
# clinician_share_links, provider_health.
# - Encrypted-at-rest payloads we cannot decrypt on edge01:
# - Encrypted-at-rest payloads we cannot decrypt on the target:
# insight_narratives, coach_messages (coach_* entirely).
# - Coach: coach_conversations, coach_messages, coach_facts, coach_usage.
# - Ops / transient / regenerable: idempotency_keys, rate_limits,
# data_backups, import_jobs, host_metrics, feedback,
# recommendation_feedback, all rollup tables (measurement_rollups,
# mood_entry_rollups, strain_trimp_cache, cycle_predictions) — these REGENERATE on edge01.
# mood_entry_rollups, strain_trimp_cache, cycle_predictions) — these REGENERATE on the target.
# - workouts / workout_routes / workout_samples — demo has none.
# - app_settings — instance-level, not user data.
# - Global catalogues (mood_tags, cycle_symptoms) — pre-exist on edge01;
# - Global catalogues (mood_tags, cycle_symptoms) — pre-exist on the target;
# carried link rows reference these GLOBAL ids only (see guards below).
#
set -euo pipefail
@@ -129,21 +129,21 @@ psql_src() {
# so psql_dst is defined after resolution.
echo "=============================================================="
echo " HealthLog demo-user migration apps01 -> edge01"
echo " HealthLog demo-user migration source -> target"
echo " demo user : $DEMO_USER_ID"
echo " src host : $SRC_SSH (container $SRC_DB_CONTAINER)"
echo " dst host : $DST_SSH (container resolved below)"
echo "=============================================================="
# ============================================================================
# RESOLVE the edge01 demo DB container (NOT hardcoded — it is a separate
# RESOLVE the target demo DB container (NOT hardcoded — it is a separate
# Coolify Postgres resource whose container name can change on redeploy).
# ============================================================================
echo
if [[ -n "${DST_DB_CONTAINER:-}" ]]; then
echo "## Using DST_DB_CONTAINER from env: $DST_DB_CONTAINER"
else
echo "## Resolving edge01 demo Postgres container ..."
echo "## Resolving target demo Postgres container ..."
DST_DB_CONTAINER="$(ssh "$DST_SSH" "docker ps --format '{{.Names}}' | grep -iE 'postgres|^db-' || true")"
echo "candidate containers on $DST_SSH:"
echo "$DST_DB_CONTAINER" | sed 's/^/ /'
@@ -159,7 +159,7 @@ else
DST_DB_CONTAINER="$(printf '%s\n' "$DST_DB_CONTAINER" | head -n1)"
fi
readonly DST_DB_CONTAINER
echo "resolved edge01 demo DB container: $DST_DB_CONTAINER"
echo "resolved target demo DB container: $DST_DB_CONTAINER"
# Confirm the psql user/db actually work on the resolved container before we
# touch anything (fail fast on a wrong container guess).
@@ -167,31 +167,31 @@ psql_dst() {
# Usage: psql_dst "<sql>"
ssh "$DST_SSH" "docker exec -i $DST_DB_CONTAINER psql -X -v ON_ERROR_STOP=1 -U $PG_USER -d $PG_DB $*"
}
echo "## Verifying edge01 DB connectivity ..."
echo "## Verifying target DB connectivity ..."
psql_dst "-c 'SELECT current_database(), current_user;'"
# ============================================================================
# RESOLVE the edge01 demo user id (it MAY differ from the apps01 id!).
# We resolve by username='demo'. After this migration the edge01 demo user
# will carry the apps01 canonical id (we delete the old row + load the apps01
# row). Scope EVERY wipe to whatever the edge01 id is right now.
# RESOLVE the target demo user id (it MAY differ from the source id!).
# We resolve by username='demo'. After this migration the target demo user
# will carry the source canonical id (we delete the old row + load the source
# row). Scope EVERY wipe to whatever the target id is right now.
# ============================================================================
echo
echo "## Resolving edge01 demo user id (by username='demo') ..."
EDGE_DEMO_ID="$(psql_dst "-tA -c \"SELECT id FROM users WHERE username = 'demo';\"" | tr -d '[:space:]')"
if [[ -z "$EDGE_DEMO_ID" ]]; then
echo "note: no existing demo user on edge01 (username='demo') — wipe phase is a no-op."
echo "## Resolving target demo user id (by username='demo') ..."
TARGET_DEMO_ID="$(psql_dst "-tA -c \"SELECT id FROM users WHERE username = 'demo';\"" | tr -d '[:space:]')"
if [[ -z "$TARGET_DEMO_ID" ]]; then
echo "note: no existing demo user on the target (username='demo') — wipe phase is a no-op."
else
echo "edge01 existing demo user id: $EDGE_DEMO_ID"
if [[ "$EDGE_DEMO_ID" == "$REAL_USER_ID" ]]; then
echo "ABORT: edge01 username='demo' resolves to the REAL operator id. Refusing."
echo "existing target demo user id: $TARGET_DEMO_ID"
if [[ "$TARGET_DEMO_ID" == "$REAL_USER_ID" ]]; then
echo "ABORT: target username='demo' resolves to the REAL operator id. Refusing."
exit 1
fi
fi
readonly EDGE_DEMO_ID
readonly TARGET_DEMO_ID
# ============================================================================
# PRE-FLIGHT GUARD (on apps01 source): assert every carried set is demo-only.
# PRE-FLIGHT GUARD (on the source): assert every carried set is demo-only.
# Each query MUST return 0. A non-zero result means a scope predicate is wrong
# and we would leak another user's rows — ABORT immediately.
# ============================================================================
@@ -243,13 +243,13 @@ preflight "mood_entry_tag_links(join)" \
# GUARD: mood_entry_tag_links must reference ONLY global mood_tags (user_id IS
# NULL on the tag). The demo is asserted to have 0 custom mood_tags; if a link
# referenced a per-user tag id, the FK would break on edge01 (that tag row is
# referenced a per-user tag id, the FK would break on the target (that tag row is
# not carried). Custom-tag count MUST be 0.
DEMO_CUSTOM_MOODTAGS="$(psql_src "-tA -c \"SELECT count(*) FROM mood_tags WHERE user_id = '$DEMO_USER_ID';\"" | tr -d '[:space:]')"
echo " demo custom mood_tags = $DEMO_CUSTOM_MOODTAGS (must be 0 — all links point to GLOBAL tags)"
if [[ "$DEMO_CUSTOM_MOODTAGS" != "0" ]]; then
echo "ABORT: demo user owns $DEMO_CUSTOM_MOODTAGS custom mood_tags. mood_entry_tag_links"
echo " would reference non-global tag ids that are NOT carried -> FK break on edge01."
echo " would reference non-global tag ids that are NOT carried -> FK break on the target."
echo " Carry mood_tags (user-scoped) before mood_entry_tag_links, or skip the links."
exit 1
fi
@@ -260,55 +260,55 @@ preflight "mood_entry_tag_links(non-global tag)" \
echo "## PRE-FLIGHT guard passed — every carried set is demo-only."
# ============================================================================
# WIPE-FIRST on edge01 (idempotent, demo-scoped ONLY). FK-child-first order.
# Every DELETE is scoped to the resolved edge01 demo user id ($EDGE_DEMO_ID)
# WIPE-FIRST on the target (idempotent, demo-scoped ONLY). FK-child-first order.
# Every DELETE is scoped to the resolved target demo user id ($TARGET_DEMO_ID)
# or via a join back to it. NEVER an unscoped DELETE. Skipped entirely if no
# existing demo user was found.
# ============================================================================
echo
if [[ -n "$EDGE_DEMO_ID" ]]; then
echo "## WIPE edge01 demo rows (scoped to $EDGE_DEMO_ID), FK-child-first ..."
# NOTE: edge01 FKs are ON DELETE CASCADE for most child rows; the explicit
if [[ -n "$TARGET_DEMO_ID" ]]; then
echo "## WIPE target demo rows (scoped to $TARGET_DEMO_ID), FK-child-first ..."
# NOTE: target FKs are ON DELETE CASCADE for most child rows; the explicit
# child-first deletes below are belt-and-braces (some child tables hang off
# medications/mood_entries, not the user) and keep the operation transparent.
psql_dst <<SQL
\set ON_ERROR_STOP on
BEGIN;
-- join-scoped children of the demo's medications
DELETE FROM reminder_phase_configs WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM medication_inventory_events WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM medication_dose_changes WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM medication_schedules WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM reminder_phase_configs WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$TARGET_DEMO_ID');
DELETE FROM medication_inventory_events WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$TARGET_DEMO_ID');
DELETE FROM medication_dose_changes WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$TARGET_DEMO_ID');
DELETE FROM medication_schedules WHERE medication_id IN (SELECT id FROM medications WHERE user_id = '$TARGET_DEMO_ID');
-- join-scoped children of the demo's mood_entries / cycle_day_logs
DELETE FROM mood_entry_tag_links WHERE mood_entry_id IN (SELECT id FROM mood_entries WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM cycle_symptom_links WHERE day_log_id IN (SELECT id FROM cycle_day_logs WHERE user_id = '$EDGE_DEMO_ID');
DELETE FROM mood_entry_tag_links WHERE mood_entry_id IN (SELECT id FROM mood_entries WHERE user_id = '$TARGET_DEMO_ID');
DELETE FROM cycle_symptom_links WHERE day_log_id IN (SELECT id FROM cycle_day_logs WHERE user_id = '$TARGET_DEMO_ID');
-- user_id-scoped tables
DELETE FROM medication_inventory_items WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM medication_side_effects WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM medication_intake_events WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM medications WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM mood_entries WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM cycle_day_logs WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM menstrual_cycles WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM cycle_profiles WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM personal_records WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM user_achievements WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM consent_receipts WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM audit_logs WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM medication_inventory_items WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM medication_side_effects WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM medication_intake_events WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM medications WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM mood_entries WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM cycle_day_logs WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM menstrual_cycles WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM cycle_profiles WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM personal_records WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM user_achievements WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM consent_receipts WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM audit_logs WHERE user_id = '$TARGET_DEMO_ID';
-- regenerable / transient tables that may carry FK rows blocking the user delete
DELETE FROM measurement_rollups WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM mood_entry_rollups WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM strain_trimp_cache WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM cycle_predictions WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM measurements WHERE user_id = '$EDGE_DEMO_ID';
DELETE FROM measurement_rollups WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM mood_entry_rollups WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM strain_trimp_cache WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM cycle_predictions WHERE user_id = '$TARGET_DEMO_ID';
DELETE FROM measurements WHERE user_id = '$TARGET_DEMO_ID';
-- finally the user row itself (any remaining ON DELETE CASCADE children of
-- the EXCLUDED set — sessions, tokens, etc. — go with it).
DELETE FROM users WHERE id = '$EDGE_DEMO_ID';
DELETE FROM users WHERE id = '$TARGET_DEMO_ID';
COMMIT;
SQL
echo "## WIPE complete."
else
echo "## WIPE skipped — no existing edge01 demo user."
echo "## WIPE skipped — no existing target demo user."
fi
# ============================================================================
@@ -403,7 +403,7 @@ readonly MENSTRUAL_CYCLE_COLS="id, user_id, start_date, end_date, period_end_dat
sync_version, deleted_at, created_at, updated_at"
# cycle_day_logs — NULL the two app-encrypted columns (keyed to prod key,
# absent on edge01). Carry the plaintext cycle fields.
# absent on the target). Carry the plaintext cycle fields.
readonly CYCLE_DAY_LOG_COLS="id, user_id, date, cycle_id, flow, intermenstrual_bleeding,
basal_body_temp_c, ovulation_test, cervical_mucus, sexual_activity, protected_sex,
pregnancy_test, progesterone_test, contraceptive, sensitive_encrypted, notes_encrypted,
@@ -472,7 +472,7 @@ copy_table() {
-c \"\\copy $table($dst_cols) FROM STDIN (FORMAT csv, HEADER false)\""
local n
n="$(psql_dst "-tA -c \"SELECT count(*) FROM $table;\"" | tr -d '[:space:]' || echo '?')"
echo " loaded; edge01 $table total rows now: $n"
echo " loaded; target $table total rows now: $n"
}
# ============================================================================
@@ -509,7 +509,7 @@ PW_OK="$(printf '%s\n' \
| ssh "$DST_SSH" "docker exec -i $DST_DB_CONTAINER \
psql -X -tA -v ON_ERROR_STOP=1 -U $PG_USER -d $PG_DB" | tr -d '[:space:]')"
if [[ "$PW_OK" != "1" ]]; then
echo "ABORT/FAIL: demo password_hash on edge01 is not the published literal (matched rows: $PW_OK)."
echo "ABORT/FAIL: demo password_hash on the target is not the published literal (matched rows: $PW_OK)."
echo " The demo login would be broken. Fix before serving the demo."
exit 1
fi
@@ -598,7 +598,7 @@ copy_table "audit_logs" "$AUDIT_LOG_COLS" \
"audit_logs WHERE user_id = '$DEMO_USER_ID' AND action LIKE 'insights.%'" "$AUDIT_LOG_COLS"
# ============================================================================
# POST-LOAD VERIFICATION (on edge01).
# POST-LOAD VERIFICATION (on the target).
# - per-table demo-user row counts (informational),
# - HARD assertion: the REAL operator id appears in NO carried table,
# - HARD assertion: every user_id-bearing carried table contains ONLY the
@@ -606,7 +606,7 @@ copy_table "audit_logs" "$AUDIT_LOG_COLS" \
# ============================================================================
echo
echo "=============================================================="
echo "## POST-LOAD verification on edge01"
echo "## POST-LOAD verification on the target"
echo "=============================================================="
echo
@@ -657,7 +657,7 @@ REAL_HITS="$(psql_dst "-tA -c \"
\"" | tr -d '[:space:]')"
echo " real-id occurrences across carried tables = $REAL_HITS"
if [[ "$REAL_HITS" != "0" ]]; then
echo "ABORT/FAIL: operator id present in $REAL_HITS rows on edge01. Investigate immediately."
echo "ABORT/FAIL: operator id present in $REAL_HITS rows on the target. Investigate immediately."
exit 1
fi
@@ -680,22 +680,22 @@ FOREIGN_HITS="$(psql_dst "-tA -c \"
+ (SELECT count(*) FROM audit_logs WHERE user_id <> '$DEMO_USER_ID');
\"" | tr -d '[:space:]')"
echo " non-demo user_id rows across carried tables = $FOREIGN_HITS"
echo " NOTE: this counts EVERY non-demo row in these tables on edge01, not just"
echo " NOTE: this counts EVERY non-demo row in these tables on the target, not just"
echo " what this script loaded. On a clean demo DB it should be 0. If the"
echo " edge01 demo DB legitimately holds other demo-only accounts, scope"
echo " target demo DB legitimately holds other demo-only accounts, scope"
echo " this assertion to the rows this run inserted before treating >0 as fatal."
if [[ "$FOREIGN_HITS" != "0" ]]; then
echo "WARNING: $FOREIGN_HITS non-demo rows exist in carried tables on edge01."
echo "WARNING: $FOREIGN_HITS non-demo rows exist in carried tables on the target."
echo " Inspect before serving the demo (see note above)."
fi
echo
echo "HARD assertion 3: the demo user exists on edge01 with role=ADMIN and the apps01 id."
echo "HARD assertion 3: the demo user exists on the target with role=ADMIN and the source id."
psql_dst "-c \"SELECT id, username, role, telegram_enabled FROM users WHERE id = '$DEMO_USER_ID';\""
echo
echo "=============================================================="
echo " DONE. Review the counts above. Demo user is role=ADMIN; the"
echo " app's DEMO_MODE must remain ON on edge01 so all mutations are"
echo " app's DEMO_MODE must remain ON on the target so all mutations are"
echo " blocked at the proxy layer while admin pages stay viewable."
echo "=============================================================="
+3 -3
View File
@@ -1,7 +1,7 @@
-- v1.5.0 demo seed — sliding-window, NOW()-anchored, full iOS+Withings story.
--
-- The pre-v1.5 demo seed shipped only WEIGHT / BLOOD_PRESSURE / PULSE /
-- BODY_FAT / SLEEP / STEPS — and the demo DB on edge-01 had drifted to
-- BODY_FAT / SLEEP / STEPS, and the demo DB had drifted to
-- a static snapshot whose last reading was ten days old. For the iOS
-- public-beta cut we need a fresh picture: data through today, every
-- type the dashboard tiles can render, a source mix that shows the
@@ -17,8 +17,8 @@
-- falls back to live SQL (the slim rollup-tier reader) and the boot
-- backfill repopulates on the next entrypoint sweep.
--
-- Usage on edge-01:
-- docker exec -i agos0oo88gsgg88kcg4swosw \
-- Usage on the demo host:
-- docker exec -i <demo-db-container> \
-- psql -U healthlog -d healthlog < seed-demo-v15.sql
BEGIN;