export const HEALTH_P0_DATASET_NAME = 'health_p0_daily_log'; export const HEALTH_P0_INSERT_COLUMNS = Object.freeze([ 'participant_code', 'log_date', 'am_measured_at', 'am_systolic', 'am_diastolic', 'am_pulse', 'pm_measured_at', 'pm_systolic', 'pm_diastolic', 'pm_pulse', 'sleep_start', 'sleep_end', 'sleep_minutes', 'sleep_source', 'weight_kg', 'symptoms', 'alcohol', 'stress', 'medication_change', 'medication_note', 'other_note', 'entry_channel', 'photo_sent', ]); export const HEALTH_P0_READ_COLUMNS = Object.freeze([ 'id', ...HEALTH_P0_INSERT_COLUMNS, 'created_at', ]); export const HEALTH_P0_CREATE_TABLE_SQL = ` CREATE TABLE IF NOT EXISTS health_p0_daily_log ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, participant_code TEXT NOT NULL, log_date DATE NOT NULL, am_measured_at TEXT NULL, am_systolic INT NULL, am_diastolic INT NULL, am_pulse INT NULL, pm_measured_at TEXT NULL, pm_systolic INT NULL, pm_diastolic INT NULL, pm_pulse INT NULL, sleep_start TEXT NULL, sleep_end TEXT NULL, sleep_minutes INT NULL, sleep_source TEXT NULL, weight_kg NUMERIC(5,1) NULL, symptoms JSONB NOT NULL DEFAULT '[]'::jsonb, alcohol TEXT NULL, stress TEXT NULL, medication_change TEXT NULL, medication_note TEXT NULL, other_note TEXT NULL, entry_channel TEXT NULL, photo_sent BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMPTZ NULL ); `.trim(); export function buildHealthP0DatasetRegistration() { return { name: HEALTH_P0_DATASET_NAME, table: HEALTH_P0_DATASET_NAME, description: 'MeMind Health P0 前置实验每日记录', actions: ['read', 'insert'], columns: { read: [...HEALTH_P0_READ_COLUMNS], insert: [...HEALTH_P0_INSERT_COLUMNS], }, }; } export function buildHealthP0BindSpecs({ password } = {}) { return { participant: { relativePath: 'public/health-p0-log.html', accessMode: 'public', datasets: { [HEALTH_P0_DATASET_NAME]: { insert: true, read: false }, }, }, researcher: { relativePath: 'public/health-p0-admin.html', accessMode: 'password', password, datasets: { [HEALTH_P0_DATASET_NAME]: { insert: false, read: true }, }, }, }; } export function assertHealthP0HtmlContract(html, { role } = {}) { const source = String(html ?? ''); const issues = []; if (!source.includes('/assets/page-data-client.js')) issues.push('missing_page_data_client'); if (/\bon(?:click|input|submit|change)\s*=/i.test(source)) issues.push('inline_event_handler'); if (/localStorage|sessionStorage|IndexedDB/i.test(source)) issues.push('browser_storage'); if (/127\.0\.0\.1:\d+/.test(source)) issues.push('hardcoded_localhost'); if (role === 'participant') { if (!source.includes(`insertRow('${HEALTH_P0_DATASET_NAME}'`) && !source.includes(`insertRow("${HEALTH_P0_DATASET_NAME}"`)) { issues.push('missing_insert'); } if (source.includes('listRows(')) issues.push('participant_must_not_read'); } if (role === 'researcher') { if (!source.includes('authenticate(')) issues.push('missing_authenticate'); if (!source.includes('listRows(')) issues.push('missing_list_rows'); } return { ok: issues.length === 0, issues }; }