<?php
// vitals.php — Curtis Duvall daily vitals entry
// CONTRACT-VERSION 0001  (moves in tandem with Curtis_Duvall_VitalsAppContract.md)
//
// Old-school server-rendered app. Post/Redirect/Get throughout: every mutation is a
// form POST that writes the whole JSON to disk, then 303-redirects to /vitals/ so the
// browser lands on a GET (refresh-safe, no duplicate submits). The entry list is
// rendered server-side from the on-disk JSON — the disk is the single source of truth;
// the browser holds no durable state.
//
// Files (all in this directory; web server user — FreeBSD: www — needs write access):
//   vitals_data.json      the record          vitals_data.bak.json  one-deep backup
//   vitals_counter.txt    next integer id     vitals.lock           write lock
//
// IDs are server-authoritative: an empty id on POST is minted from the counter; a
// populated id is left alone (counter does not move). The id is opaque, never a date.

declare(strict_types=1);

const CONTRACT_VERSION = '0001';
const REDIRECT_TO   = '/vitals/';            // DirectoryIndex serves vitals.php
const DATA_FILE     = __DIR__ . '/vitals_data.json';
const BAK_FILE      = __DIR__ . '/vitals_data.bak.json';
const COUNTER_FILE  = __DIR__ . '/vitals_counter.txt';
const LOCK_FILE     = __DIR__ . '/vitals.lock';
const VALID_STATUS  = ['ready', 'submitted', 'acked', 'amended'];

// field lists mirrored on the client
const NUM_FIELDS = ['bp_am_sys','bp_am_dia','pulse_am','glucose_am','weight','weight_white',
                    'weight_white_pm','bp_pm_sys','bp_pm_dia','pulse_pm','ortho_sys','ortho_dia','ortho_pulse'];
const TIME_FIELDS = ['glucose_time','weigh_time','weigh_pm_time','bp_am_time','bp_pm_time','grip_time'];

// ---------------------------------------------------------------- helpers
function load_data(): array {
    if (!is_file(DATA_FILE)) return [];
    $raw = file_get_contents(DATA_FILE);
    $p = json_decode($raw, true);
    return is_array($p) ? $p : [];
}

function with_lock(callable $fn) {
    $lock = fopen(LOCK_FILE, 'c');
    if ($lock === false || !flock($lock, LOCK_EX)) {
        return [false, 'Could not acquire the write lock — nothing changed.'];
    }
    try { return $fn(); }
    finally { flock($lock, LOCK_UN); fclose($lock); }
}

// Atomically write the whole array to disk. Assigns ids to id-less rows from the
// counter (once each). Must be called INSIDE with_lock().
function write_data_locked(array $data): array {
    // read counter
    $next = 1;
    if (is_file(COUNTER_FILE)) {
        $c = trim((string)@file_get_contents(COUNTER_FILE));
        if ($c !== '' && ctype_digit($c)) { $next = max(1, (int)$c); }
    }
    // collect ids already present
    $seen = [];
    foreach ($data as $row) { if (isset($row['id'])) $seen[$row['id']] = true; }
    // mint for id-less rows
    $assigned = 0;
    foreach ($data as $i => $row) {
        if (!isset($row['id'])) {
            while (isset($seen[$next])) { $next++; }
            $data[$i]['id'] = $next; $seen[$next] = true; $next++; $assigned++;
        }
    }
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    if ($json === false) return [false, 'Could not encode data — nothing written.', $data];

    if (is_file(DATA_FILE)) { @copy(DATA_FILE, BAK_FILE); }
    $tmp = tempnam(__DIR__, 'vitals_');
    if ($tmp === false) return [false, 'Cannot create temp file — check directory write permission (needs user: www).', $data];
    if (file_put_contents($tmp, $json, LOCK_EX) === false) { @unlink($tmp); return [false, 'Write failed — nothing changed.', $data]; }
    @chmod($tmp, 0644);
    if (!@rename($tmp, DATA_FILE)) { @unlink($tmp); return [false, 'Rename failed — nothing changed.', $data]; }

    if ($assigned > 0) {
        $ctmp = tempnam(__DIR__, 'vctr_');
        if ($ctmp !== false && file_put_contents($ctmp, (string)$next, LOCK_EX) !== false) {
            @chmod($ctmp, 0644); @rename($ctmp, COUNTER_FILE);
        } else { @file_put_contents(COUNTER_FILE, (string)$next, LOCK_EX); if ($ctmp !== false) @unlink($ctmp); }
    }
    return [true, '', $data];
}

function redirect(string $flash = '', string $kind = ''): void {
    // one-shot flash via query string (consumed on the GET, then it's just history)
    $q = [];
    if ($flash !== '') { $q['m'] = $flash; if ($kind) $q['k'] = $kind; }
    $url = REDIRECT_TO . ($q ? '?' . http_build_query($q) : '');
    header('Location: ' . $url, true, 303);
    exit;
}

// build a row array from POSTed form fields
function row_from_post(array $p): array {
    $e = [];
    $e['date'] = isset($p['date']) && is_string($p['date']) ? trim($p['date']) : '';
    foreach (NUM_FIELDS as $f) {
        $v = $p[$f] ?? '';
        $e[$f] = ($v === '' || $v === null) ? null : (is_numeric($v) ? 0 + $v : null);
    }
    foreach (TIME_FIELDS as $f) {
        $v = $p[$f] ?? '';
        $e[$f] = ($v === '' || $v === null) ? null : (string)$v;
    }
    $gr = [];
    foreach (['grip_1','grip_2','grip_3'] as $g) { $v = $p[$g] ?? ''; $gr[] = ($v === '' ? null : (is_numeric($v) ? 0 + $v : null)); }
    $gl = [];
    foreach (['grip_l1','grip_l2','grip_l3'] as $g) { $v = $p[$g] ?? ''; $gl[] = ($v === '' ? null : (is_numeric($v) ? 0 + $v : null)); }
    $e['grip_r'] = $gr;
    $e['grip_l'] = $gl;
    $e['notes_am'] = isset($p['notes_am']) ? trim((string)$p['notes_am']) : '';
    $e['notes_pm'] = isset($p['notes_pm']) ? trim((string)$p['notes_pm']) : '';
    return $e;
}

// ================================================================ POST handlers
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';

    // ---- add / update / amend a single row -------------------------------
    if ($action === 'save') {
        $e = row_from_post($_POST);
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $e['date'])) { redirect('A valid date is required — nothing saved.', 'bad'); }

        $idRaw = $_POST['id'] ?? '';
        $hasId = ($idRaw !== '' && ctype_digit((string)$idRaw));
        $amend = (($_POST['amend'] ?? '') === '1');

        $res = with_lock(function () use ($e, $hasId, $idRaw, $amend) {
            $data = load_data();

            if (!$hasId) {
                // NEW row — duplicate-date is a HARD BLOCK
                foreach ($data as $row) {
                    if (($row['date'] ?? '') === $e['date']) {
                        return [false, 'DUPLICATE DATE ' . $e['date'] . ' (recommend editing existing).'];
                    }
                }
                $e['status'] = 'ready';           // id minted on write
                $data[] = $e;
            } else {
                $id = (int)$idRaw;
                $idx = -1;
                foreach ($data as $i => $row) { if (($row['id'] ?? null) === $id) { $idx = $i; break; } }
                if ($idx === -1) return [false, 'The row being edited no longer exists.'];
                // if the date was changed to one that collides with a DIFFERENT row, block
                foreach ($data as $i => $row) {
                    if ($i !== $idx && ($row['date'] ?? '') === $e['date']) {
                        return [false, 'DUPLICATE DATE ' . $e['date'] . ' (recommend editing existing).'];
                    }
                }
                $e['id'] = $id;
                $cur = $data[$idx]['status'] ?? 'ready';
                $e['status'] = $amend ? 'amended' : $cur;  // amend: acked→amended; edit keeps state
                $data[$idx] = $e;
            }

            [$ok, $err, $saved] = write_data_locked($data);
            return $ok ? [true, ''] : [false, $err];
        });

        [$ok, $msg] = $res;
        redirect($ok ? 'Saved ' . $e['date'] . '.' : $msg, $ok ? 'ok' : 'bad');
    }

    // ---- delete (only an acked row) --------------------------------------
    if ($action === 'delete') {
        $id = (int)($_POST['id'] ?? 0);
        $res = with_lock(function () use ($id) {
            $data = load_data();
            $idx = -1;
            foreach ($data as $i => $row) { if (($row['id'] ?? null) === $id) { $idx = $i; break; } }
            if ($idx === -1) return [false, 'Row not found.'];
            if (($data[$idx]['status'] ?? '') !== 'acked') return [false, 'Only an acked row can be deleted.'];
            $date = $data[$idx]['date'] ?? '';
            array_splice($data, $idx, 1);
            [$ok, $err] = write_data_locked($data);
            return $ok ? [true, 'Deleted ' . $date . '.'] : [false, $err];
        });
        [$ok, $msg] = $res;
        redirect($msg, $ok ? 'ok' : 'bad');
    }

    // ---- export flip: ready→submitted, then land on a page that streams it -
    if ($action === 'export') {
        $id = (int)($_POST['id'] ?? 0);
        $res = with_lock(function () use ($id) {
            $data = load_data();
            $idx = -1;
            foreach ($data as $i => $row) { if (($row['id'] ?? null) === $id) { $idx = $i; break; } }
            if ($idx === -1) return [false, 'Row not found.'];
            $st = $data[$idx]['status'] ?? '';
            if ($st === 'ready') {
                $data[$idx]['status'] = 'submitted';
                [$ok, $err] = write_data_locked($data);
                return $ok ? [true, ''] : [false, $err];
            } elseif ($st === 'submitted' || $st === 'amended') {
                return [true, ''];  // already in an exportable state; no flip needed
            }
            return [false, 'This row is ' . $st . ' — nothing to export.'];
        });
        [$ok, $msg] = $res;
        if (!$ok) { redirect($msg, 'bad'); }
        // land on a page that will stream the (now current) row from disk
        header('Location: ' . REDIRECT_TO . '?exported=' . $id, true, 303);
        exit;
    }

    // ---- apply an ack (ids the lane confirmed are in canon) ---------------
    if ($action === 'ack') {
        $idsRaw = $_POST['acked_ids'] ?? '';
        $ver    = $_POST['contract_version'] ?? '';
        if ($ver !== CONTRACT_VERSION) { redirect('CONTRACT MISMATCH: ack ' . $ver . ' vs app ' . CONTRACT_VERSION . '. Nothing changed.', 'bad'); }
        $ids = array_values(array_filter(array_map('trim', explode(',', (string)$idsRaw)), fn($x) => $x !== '' && ctype_digit($x)));
        if (!$ids) { redirect('Ack had no valid ids — nothing changed.', 'bad'); }

        $res = with_lock(function () use ($ids) {
            $data = load_data();
            $byId = [];
            foreach ($data as $i => $row) { if (isset($row['id'])) $byId[$row['id']] = $i; }
            $done = []; $skip = [];
            foreach ($ids as $ids2) {
                $id = (int)$ids2;
                if (!isset($byId[$id])) { $skip[] = '#' . $id . ' (no row)'; continue; }
                $i = $byId[$id]; $st = $data[$i]['status'] ?? '';
                if ($st === 'submitted' || $st === 'amended') { $data[$i]['status'] = 'acked'; $done[] = '#' . $id . ' ' . ($data[$i]['date'] ?? ''); }
                else { $skip[] = '#' . $id . ' (' . $st . ', not awaiting ack)'; }
            }
            if (!$done) return [false, 'No rows acked. ' . ($skip ? 'Skipped: ' . implode('; ', $skip) : '')];
            [$ok, $err] = write_data_locked($data);
            if (!$ok) return [false, $err];
            return [true, 'Acked ' . implode(', ', $done) . '.' . ($skip ? ' Skipped: ' . implode('; ', $skip) : '')];
        });
        [$ok, $msg] = $res;
        redirect($msg, $ok ? 'ok' : 'bad');
    }

    // unknown action
    redirect('Unknown action — nothing changed.', 'bad');
}

// ================================================================ GET (render)
$entries = load_data();
usort($entries, fn($a, $b) => strcmp($a['date'] ?? '', $b['date'] ?? ''));
$writable = is_writable(__DIR__);

$flash = $_GET['m'] ?? '';
$flashKind = $_GET['k'] ?? '';
$exportedId = (isset($_GET['exported']) && ctype_digit((string)$_GET['exported'])) ? (int)$_GET['exported'] : null;

// build the export payload for the just-exported row (streamed client-side on load)
$exportPayload = null;
if ($exportedId !== null) {
    foreach ($entries as $row) {
        if (($row['id'] ?? null) === $exportedId) {
            $r = $row;
            if (($r['status'] ?? '') === 'ready') $r['status'] = 'submitted';
            $exportPayload = [
                'contract_version' => CONTRACT_VERSION,
                'kind' => 'vitals_export',
                'row' => $r,
            ];
            break;
        }
    }
}

// helpers for rendering
function h($s): string { return htmlspecialchars((string)$s, ENT_QUOTES); }
function numOr($v, $dash = '—') { return ($v === null || $v === '') ? $dash : h($v); }
function fmtBP($s, $d, $p): string {
    $parts = [];
    if ($s !== null && $d !== null) $parts[] = '<b>' . h($s) . '/' . h($d) . '</b>';
    if ($p !== null) $parts[] = 'pulse <b>' . h($p) . '</b>';
    return $parts ? implode(' · ', $parts) : '—';
}
function actionsFor(array $e): string {
    $id = (int)($e['id'] ?? 0);
    $st = $e['status'] ?? 'ready';
    $btn = fn($html) => $html;
    switch ($st) {
        case 'acked':
            return post_btn('amend_start', $id, 'Amend', 'amend') . post_btn('delete', $id, 'Delete', 'del');
        case 'amended':
            return post_btn('amend_start', $id, 'Amend', 'amend') . post_btn('export', $id, 'Export', '');
        case 'submitted':
        case 'ready':
        default:
            return post_btn('edit_start', $id, 'Edit', '') . post_btn('export', $id, 'Export', '');
    }
}
// Edit/Amend "start" don't mutate — they're client actions (load the form). We render
// them as buttons that call JS. Export/Delete are real POSTs.
function post_btn(string $act, int $id, string $label, string $cls): string {
    $c = $cls ? ' ' . $cls : '';
    if ($act === 'edit_start' || $act === 'amend_start') {
        $amend = $act === 'amend_start' ? '1' : '0';
        return '<button type="button" class="rowbtn' . $c . '" data-editstart="' . $id . '" data-amend="' . $amend . '">' . h($label) . '</button>';
    }
    if ($act === 'export') {
        return '<form method="post" style="display:inline"><input type="hidden" name="action" value="export"><input type="hidden" name="id" value="' . $id . '"><button type="submit" class="rowbtn' . $c . '">' . h($label) . '</button></form>';
    }
    if ($act === 'delete') {
        return '<form method="post" style="display:inline" onsubmit="return confirm(\'Delete this entry? It is in canon, so this only removes the app copy.\')"><input type="hidden" name="action" value="delete"><input type="hidden" name="id" value="' . $id . '"><button type="submit" class="rowbtn' . $c . '">' . h($label) . '</button></form>';
    }
    return '';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Vitals">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#14161a">
<title>Vitals</title>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="apple-touch-icon-precomposed" sizes="180x180" href="apple-touch-icon.png">
<style>
  :root {
    --bg:#14161a; --panel:#1c1f25; --panel-2:#232730; --line:#333945;
    --text:#e6e8ec; --dim:#939aa8; --accent:#5aa9e6; --ok:#4caf7d; --warn:#d9a441; --bad:#d95f5f;
  }
  * { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  body { margin:0; padding:0 0 3rem; background:var(--bg); color:var(--text);
    font:16px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
  header { padding:1rem 1rem 0.75rem; border-bottom:1px solid var(--line);
    display:flex; align-items:baseline; justify-content:space-between; gap:1rem; }
  h1 { font-size:1.05rem; margin:0; font-weight:600; letter-spacing:0.01em; }
  main { padding:1rem; max-width:720px; margin:0 auto; }
  .card { background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:1rem; margin-bottom:1rem; }
  fieldset { border:0; margin:0 0 1rem; padding:0; }
  fieldset:last-of-type { margin-bottom:0; }
  legend { font-size:0.72rem; text-transform:uppercase; letter-spacing:0.09em; color:var(--dim); padding:0; margin-bottom:0.5rem; }
  label { display:block; font-size:0.78rem; color:var(--dim); margin-bottom:0.25rem; }
  input, textarea { width:100%; background:var(--panel-2); border:1px solid var(--line); border-radius:7px;
    color:var(--text); padding:0.6rem 0.65rem; font:inherit; font-size:1rem; }
  input:focus, textarea:focus { outline:2px solid var(--accent); outline-offset:-1px; }
  input.dupe { border-color:var(--bad); outline-color:var(--bad); }
  textarea { min-height:15rem; resize:vertical; }
  .row { display:flex; gap:0.6rem; margin-bottom:0.75rem; }
  .row:last-child { margin-bottom:0; }
  .row > div { flex:1 1 0; min-width:0; }
  .row > div input { min-width:0; max-width:100%; }
  input[inputmode="decimal"] { width:5.5ch; max-width:5.5ch; min-width:5.5ch; text-align:center; }
  input[type="number"] { -moz-appearance:textfield; appearance:textfield; }
  input[type="number"]::-webkit-outer-spin-button,
  input[type="number"]::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
  .row > div label { overflow-wrap:break-word; }
  .btns { display:flex; gap:0.6rem; flex-wrap:wrap; }
  button { background:var(--panel-2); color:var(--text); border:1px solid var(--line); border-radius:7px;
    padding:0.7rem 1.1rem; font:inherit; font-weight:600; cursor:pointer; }
  button:active { transform:translateY(1px); }
  button.primary { background:var(--accent); border-color:var(--accent); color:#0d1117; }
  button.ghost { color:var(--dim); }
  .msg { margin-top:0.75rem; font-size:0.85rem; min-height:1.2em; }
  .msg.warn { color:var(--warn); } .msg.bad { color:var(--bad); } .msg.ok { color:var(--ok); }
  .flash { padding:0.7rem 0.9rem; border-radius:8px; font-size:0.85rem; margin-bottom:1rem; border:1px solid var(--line); }
  .flash.ok { background:#1e2b23; border-color:var(--ok); color:#bfe6d0; }
  .flash.bad { background:#3a2323; border-color:var(--bad); color:#f2c3c3; }
  .seg { display:flex; margin-bottom:1rem; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
  .seg button { flex:1; border:0; border-radius:0; background:var(--panel-2); color:var(--dim); padding:0.6rem; font-weight:700; letter-spacing:0.03em; }
  .seg button.active { background:var(--accent); color:#0d1117; }
  .seg-ico { font-size:0.95rem; }
  .box-title { font-size:0.8rem; font-weight:700; color:var(--text); margin-bottom:0.75rem; padding-bottom:0.5rem; border-bottom:1px solid var(--line); }
  .time-wrap { display:flex; gap:0.4rem; }
  .time-wrap input { flex:1 1 0; min-width:0; }
  .now { flex:0 0 auto; padding:0.6rem 0.7rem; font-size:0.8rem; font-weight:600; background:var(--panel-2); border:1px solid var(--line); color:var(--accent); }
  .view-am, .view-pm { display:none; }
  body.mode-am .view-am { display:block; }
  body.mode-pm .view-pm { display:block; }
  .sx { display:flex; gap:0.5rem; flex-wrap:wrap; margin-bottom:0.6rem; }
  .sx button { flex:1 1 auto; padding:0.55rem 0.6rem; font-size:0.85rem; font-weight:500;
    background:var(--panel-2); border:1px dashed var(--line); color:var(--text); min-width:6rem; }
  .hint { font-size:0.72rem; color:var(--dim); margin:-0.3rem 0 0.5rem; }
  .dev-series + .hint { margin-top:0.5rem; }
  .prompt { display:none; background:#3a3320; border:1px solid var(--warn); color:#f0d9a8;
    padding:0.55rem 0.75rem; border-radius:7px; font-size:0.82rem; margin-bottom:0.75rem; }
  .prompt.show { display:block; }
  .interval { font-size:0.78rem; color:var(--dim); margin:-0.3rem 0 0.75rem; }
  .interval b { color:var(--text); }
  .disclose { margin-top:0.5rem; }
  .disclose > summary { cursor:pointer; font-size:0.8rem; color:var(--accent); list-style:none; padding:0.4rem 0; user-select:none; }
  .disclose > summary::-webkit-details-marker { display:none; }
  .disclose > summary::before { content:"▸ "; }
  .disclose[open] > summary::before { content:"▾ "; }
  .safety { background:#3a2323; border:1px solid var(--bad); color:#f2c3c3; padding:0.55rem 0.75rem; border-radius:7px; font-size:0.8rem; margin:0.4rem 0 0.75rem; }

  #amend-banner { display:none; background:var(--bad); color:#1a0d0d; font-weight:800;
    letter-spacing:0.12em; text-align:center; padding:0.6rem; border-radius:8px; margin-bottom:1rem; font-size:0.95rem; }
  body.amending #amend-banner { display:block; }
  body.amending #form-card .card { border-color:var(--bad); box-shadow:0 0 0 1px var(--bad) inset; }
  body.amending #form-card input, body.amending #form-card textarea { border-color:var(--bad); }
  body.amending #form-card .seg { border-color:var(--bad); }

  .entry { background:var(--panel); border:1px solid var(--line); border-radius:10px; margin-bottom:0.6rem; overflow:hidden; }
  .entry.st-acked { border-left:3px solid var(--ok); }
  .entry.st-amended { border-left:3px solid var(--bad); }
  .entry.st-submitted { border-left:3px solid var(--accent); }
  .entry.st-ready { border-left:3px solid var(--dim); }
  .entry-head { display:flex; justify-content:space-between; align-items:center; gap:0.5rem; padding:0.75rem 1rem; cursor:pointer; user-select:none; }
  .entry-head-left { display:flex; align-items:center; gap:0.6rem; min-width:0; flex-wrap:wrap; }
  .caret { color:var(--dim); font-size:0.8rem; flex:0 0 auto; transition:transform 0.12s; }
  .entry.open .caret { transform:rotate(90deg); }
  .entry-date { font-weight:700; font-size:1rem; }
  .entry-id { color:var(--dim); font-size:0.72rem; }
  .badge { font-size:0.66rem; text-transform:uppercase; letter-spacing:0.07em; font-weight:700; padding:0.12rem 0.5rem; border-radius:999px; border:1px solid var(--line); color:var(--dim); }
  .badge.ready { color:var(--dim); border-color:var(--dim); }
  .badge.submitted { color:var(--accent); border-color:var(--accent); }
  .badge.acked { color:var(--ok); border-color:var(--ok); }
  .badge.amended { color:var(--bad); border-color:var(--bad); }
  .entry-actions { display:flex; gap:0.4rem; flex:0 0 auto; }
  .entry-actions .rowbtn { padding:0.35rem 0.7rem; font-size:0.8rem; font-weight:500; }
  .entry-actions .del, .entry-actions .amend { color:var(--bad); border-color:var(--bad); }
  .entry-detail { display:none; padding:0 1rem 0.85rem; }
  .entry.open .entry-detail { display:block; }
  .entry-body { margin-top:0.25rem; font-size:0.85rem; color:var(--dim); display:grid; gap:0.2rem; }
  .entry-body b { color:var(--text); font-weight:600; }
  .partial { color:var(--warn); font-size:0.78rem; }
  .entry-notes { margin-top:0.4rem; font-size:0.85rem; color:var(--text); white-space:pre-wrap; }
  .empty { color:var(--dim); font-size:0.9rem; padding:1rem 0; text-align:center; }
  .banner { background:#3a2323; border:1px solid var(--bad); color:#f2c3c3; padding:0.7rem 0.9rem; border-radius:8px; font-size:0.85rem; margin-bottom:1rem; }
  .section-title { font-size:0.72rem; text-transform:uppercase; letter-spacing:0.09em; color:var(--dim); margin:1.5rem 0 0.6rem; }
  .editing-flag { font-size:0.78rem; color:var(--accent); margin-bottom:0.75rem; }
  .selfserve-note { font-size:0.75rem; color:var(--dim); font-style:italic; margin-bottom:0.75rem; }
  .hidden { display:none; }
  #ack-zone { border:1px dashed var(--line); border-radius:9px; padding:0.9rem 1rem; text-align:center; color:var(--dim); font-size:0.85rem; margin:1rem 0; transition:border-color 0.12s, background 0.12s; }
  #ack-zone.drag { border-color:var(--accent); background:rgba(90,169,230,0.08); color:var(--text); }
  #ack-zone b { color:var(--text); }
  .dev-series { display:flex; align-items:flex-end; gap:0.6rem; }
  .dev-series > .dev-ico { flex:0 0 44px; width:44px; height:44px; object-fit:contain; margin-bottom:0.6rem; filter:drop-shadow(0 0 1px rgba(0,0,0,0.4)); }
  .dev-series > .row { flex:1 1 auto; margin-bottom:0; }
</style>
</head>
<body>

<header>
  <h1>Curtis Duvall — Vitals</h1>
  <span class="entry-id">contract <?= CONTRACT_VERSION ?></span>
</header>

<main>

<?php if ($flash): ?>
  <div class="flash <?= $flashKind === 'bad' ? 'bad' : 'ok' ?>"><?= h($flash) ?></div>
<?php endif; ?>
<?php if (!$writable): ?>
  <div class="banner">This directory is not writable by the web server. Save will fail. Fix: <code>chown www <?= h(__DIR__) ?></code></div>
<?php endif; ?>

<!-- ============ ENTRY FORM (POSTs action=save) ============ -->
<form id="entry-form" method="post">
<input type="hidden" name="action" value="save">
<input type="hidden" name="id" id="f_id" value="">
<input type="hidden" name="amend" id="f_amend" value="0">

<div id="form-card">
  <div id="amend-banner">⚠ AMENDMENT — changing a record already in canon ⚠</div>
  <div id="editing-flag" class="editing-flag hidden"></div>

  <div class="seg">
    <button type="button" id="seg-am"><span class="seg-ico">☀</span> AM</button>
    <button type="button" id="seg-pm"><span class="seg-ico">☾</span> PM</button>
  </div>

  <div class="card">
    <fieldset>
      <legend>Date</legend>
      <div class="row"><div><input type="date" name="date" id="date"></div></div>
    </fieldset>
  </div>

  <div class="view-am">
    <div class="card">
      <div class="box-title">Self-serve — he does these (his wake)</div>
      <fieldset>
        <div class="dev-series">
          <img src="glucometer.png" class="dev-ico" alt="Accu-Chek glucose meter">
          <div class="row">
            <div><label for="glucose_time">Glucose time</label>
              <div class="time-wrap"><input type="time" name="glucose_time" id="glucose_time"></div></div>
            <div><label for="glucose_am">Fasting glucose</label><input type="number" inputmode="decimal" name="glucose_am" id="glucose_am"></div>
          </div>
        </div>
        <div class="dev-series">
          <img src="taylor-scale.png" class="dev-ico" alt="Taylor scale">
          <div class="row">
            <div><label for="weigh_time">Weigh time</label>
              <div class="time-wrap"><input type="time" name="weigh_time" id="weigh_time"></div></div>
            <div><label for="weight">Old Black Scale (lb)</label><input type="number" inputmode="decimal" step="0.1" name="weight" id="weight"></div>
          </div>
        </div>
        <details class="disclose">
          <summary>New White Scale — dual-weigh (temporary)</summary>
          <div class="safety" style="background:#232730;border-color:var(--line);color:var(--dim)">Weigh on BOTH scales the same morning for about a week. Record each raw — do not adjust either. The consistent difference is the offset, so the switch is a documented step, not a mystery jump. Remove this once the Old Black Scale is retired.</div>
          <div class="dev-series">
            <img src="withings-scale.png" class="dev-ico" alt="Withings scale">
            <div class="row">
              <div><label for="weight_white">New White Scale (lb)</label><input type="number" inputmode="decimal" step="0.1" name="weight_white" id="weight_white"></div>
            </div>
          </div>
        </details>
        <div class="selfserve-note">Pills taken here (with the glucose test) — inferred from the glucose time, not a separate field.</div>
      </fieldset>
    </div>

    <div class="card">
      <div class="box-title">Assisted — you do these (your wake, post-meds)</div>
      <fieldset>
        <legend>AM blood pressure</legend>
        <div id="prompt-am" class="prompt">Systolic is high — ask him about ear pain, and record the answer either way.</div>
        <div class="dev-series">
          <img src="bp-monitor.png" class="dev-ico" alt="BP monitor">
          <div class="row">
            <div><label for="bp_am_sys">BP sys</label><input type="number" inputmode="decimal" name="bp_am_sys" id="bp_am_sys"></div>
            <div><label for="bp_am_dia">BP dia</label><input type="number" inputmode="decimal" name="bp_am_dia" id="bp_am_dia"></div>
            <div><label for="pulse_am">Pulse</label><input type="number" inputmode="decimal" name="pulse_am" id="pulse_am"></div>
          </div>
        </div>
        <div class="row">
          <div><label for="bp_am_time">BP time</label>
            <div class="time-wrap"><input type="time" name="bp_am_time" id="bp_am_time"><button type="button" class="now" data-now="bp_am_time">Now</button></div></div>
        </div>
        <div id="interval" class="interval hidden"></div>
      </fieldset>

      <fieldset class="grip">
        <legend>Grip — right hand (kg), after AM BP</legend>
        <div class="dev-series">
          <img src="dynamometer.png" class="dev-ico" alt="Hand dynamometer">
          <div class="row">
            <div><label for="grip_1">Squeeze 1</label><input type="number" inputmode="decimal" step="0.1" name="grip_1" id="grip_1"></div>
            <div><label for="grip_2">Squeeze 2</label><input type="number" inputmode="decimal" step="0.1" name="grip_2" id="grip_2"></div>
            <div><label for="grip_3">Squeeze 3</label><input type="number" inputmode="decimal" step="0.1" name="grip_3" id="grip_3"></div>
          </div>
        </div>
        <div class="row">
          <div><label for="grip_time">Grip time</label>
            <div class="time-wrap"><input type="time" name="grip_time" id="grip_time"><button type="button" class="now" data-now="grip_time">Now</button></div></div>
        </div>
        <details class="disclose">
          <summary>Left hand — for the both-hands comparison</summary>
          <div class="safety" style="background:#232730;border-color:var(--line);color:var(--dim)">Both arms carry hardware (right: shoulder screw + forearm rod; left: humeral staples), so neither hand is a clean site. Record both for a short baseline to see which reads higher / more consistently, then settle on one primary and close this. Same coached-max method; fill what you collect.</div>
          <div class="row">
            <div><label for="grip_l1">Left squeeze 1</label><input type="number" inputmode="decimal" step="0.1" name="grip_l1" id="grip_l1"></div>
            <div><label for="grip_l2">Left squeeze 2</label><input type="number" inputmode="decimal" step="0.1" name="grip_l2" id="grip_l2"></div>
            <div><label for="grip_l3">Left squeeze 3</label><input type="number" inputmode="decimal" step="0.1" name="grip_l3" id="grip_l3"></div>
          </div>
        </details>
      </fieldset>

      <fieldset>
        <legend>Symptom check (AM) — tap to add a line, then finish the sentence</legend>
        <div class="sx">
          <button type="button" data-sx="dizzy" data-tgt="notes_am">Dizzy</button>
          <button type="button" data-sx="lightheaded" data-tgt="notes_am">Lightheaded</button>
          <button type="button" data-sx="headache" data-tgt="notes_am">Headache</button>
          <button type="button" data-sx="ear pain" data-tgt="notes_am">Ear pain</button>
          <button type="button" data-sx="cramps" data-tgt="notes_am">Cramps</button>
          <button type="button" data-sx="confusion" data-tgt="notes_am">Confusion</button>
        </div>
        <div class="hint">Buttons type a label into the AM notes below — nothing is stored but the text you write.</div>
        <textarea name="notes_am" id="notes_am" rows="12" placeholder="AM symptoms, context, anything worth a line."></textarea>
      </fieldset>
    </div>
  </div>

  <div class="view-pm">
    <div class="card">
      <div class="box-title">Self-serve — PM (his bedtime)</div>
      <fieldset>
        <legend>Evening weigh — New White Scale only</legend>
        <div class="dev-series">
          <img src="withings-scale.png" class="dev-ico" alt="Withings scale">
          <div class="row">
            <div><label for="weigh_pm_time">Weigh time</label>
              <div class="time-wrap"><input type="time" name="weigh_pm_time" id="weigh_pm_time"></div></div>
            <div><label for="weight_white_pm">New White Scale (lb)</label><input type="number" inputmode="decimal" step="0.1" name="weight_white_pm" id="weight_white_pm"></div>
          </div>
        </div>
        <div class="hint">His before-bed weigh. White scale only — read from the Withings app (or the pull button, once wired). Separate PM series; not compared against morning weights.</div>
      </fieldset>
    </div>

    <div class="card">
      <div class="box-title">Assisted — PM (~2 h after dinner)</div>
      <fieldset>
        <legend>PM blood pressure</legend>
        <div id="prompt-pm" class="prompt">Systolic is high — ask him about ear pain, and record the answer either way.</div>
        <div class="dev-series">
          <img src="bp-monitor.png" class="dev-ico" alt="BP monitor">
          <div class="row">
            <div><label for="bp_pm_sys">BP sys</label><input type="number" inputmode="decimal" name="bp_pm_sys" id="bp_pm_sys"></div>
            <div><label for="bp_pm_dia">BP dia</label><input type="number" inputmode="decimal" name="bp_pm_dia" id="bp_pm_dia"></div>
            <div><label for="pulse_pm">Pulse</label><input type="number" inputmode="decimal" name="pulse_pm" id="pulse_pm"></div>
          </div>
        </div>
        <div class="row">
          <div><label for="bp_pm_time">BP time</label>
            <div class="time-wrap"><input type="time" name="bp_pm_time" id="bp_pm_time"><button type="button" class="now" data-now="bp_pm_time">Now</button></div></div>
        </div>
        <details class="disclose">
          <summary>Orthostatic (standing) check — only on good days</summary>
          <div class="safety">Fall risk. He is not stable standing. Only on a good day, with someone within arm's reach and support ready. Abort at the first wobble. The seated comparator is the PM BP above.</div>
          <div class="row">
            <div><label for="ortho_sys">Standing sys</label><input type="number" inputmode="decimal" name="ortho_sys" id="ortho_sys"></div>
            <div><label for="ortho_dia">Standing dia</label><input type="number" inputmode="decimal" name="ortho_dia" id="ortho_dia"></div>
            <div><label for="ortho_pulse">Standing pulse</label><input type="number" inputmode="decimal" name="ortho_pulse" id="ortho_pulse"></div>
          </div>
        </details>
      </fieldset>

      <fieldset>
        <legend>Symptom check (PM) — tap to add a line, then finish the sentence</legend>
        <div class="sx">
          <button type="button" data-sx="dizzy" data-tgt="notes_pm">Dizzy</button>
          <button type="button" data-sx="lightheaded" data-tgt="notes_pm">Lightheaded</button>
          <button type="button" data-sx="headache" data-tgt="notes_pm">Headache</button>
          <button type="button" data-sx="ear pain" data-tgt="notes_pm">Ear pain</button>
          <button type="button" data-sx="cramps" data-tgt="notes_pm">Cramps</button>
          <button type="button" data-sx="confusion" data-tgt="notes_pm">Confusion</button>
        </div>
        <div class="hint">Buttons type a label into the PM notes below — nothing is stored but the text you write.</div>
        <textarea name="notes_pm" id="notes_pm" rows="12" placeholder="PM symptoms, context, anything worth a line."></textarea>
      </fieldset>
    </div>
  </div>

  <div class="card">
    <div class="btns">
      <button type="submit" class="primary" id="btn-add">Add</button>
      <button type="button" class="ghost hidden" id="btn-cancel">Cancel</button>
      <button type="button" class="ghost" id="btn-clear">Clear form</button>
    </div>
    <div class="msg" id="form-msg"></div>
  </div>
</div>
</form>

<div id="ack-zone">
  Drop an <b>ack .json</b> here (from the Vitals lane) to mark rows as recorded in canon.
</div>

<!-- hidden ack POST form; JS fills acked_ids from the dropped file, then submits -->
<form id="ack-form" method="post" class="hidden">
  <input type="hidden" name="action" value="ack">
  <input type="hidden" name="contract_version" id="ack_ver" value="">
  <input type="hidden" name="acked_ids" id="ack_ids" value="">
</form>

<div class="section-title">Entries — newest first (<?= count($entries) ?>)</div>
<div id="list">
<?php if (!$entries): ?>
  <div class="empty">No entries yet.</div>
<?php else: foreach (array_reverse($entries) as $e):
    $st = $e['status'] ?? 'ready';
    $id = (int)($e['id'] ?? 0);
    $gr = array_values(array_filter($e['grip_r'] ?? [], fn($g) => $g !== null));
    $gl = array_values(array_filter($e['grip_l'] ?? [], fn($g) => $g !== null));
    $amMissing = !isset($e['bp_am_sys']) || $e['bp_am_sys'] === null;
    $pmMissing = !isset($e['bp_pm_sys']) || $e['bp_pm_sys'] === null;
?>
  <div class="entry st-<?= h($st) ?>" data-row="<?= $id ?>"
       data-json='<?= h(json_encode($e, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP)) ?>'>
    <div class="entry-head">
      <span class="entry-head-left">
        <span class="caret">▶</span>
        <span class="entry-date"><?= h($e['date'] ?? '') ?></span>
        <span class="entry-id">#<?= $id ?></span>
        <span class="badge <?= h($st) ?>"><?= h($st) ?></span>
      </span>
      <span class="entry-actions"><?= actionsFor($e) ?></span>
    </div>
    <div class="entry-detail">
      <?php if ($amMissing && !$pmMissing): ?><div class="partial">AM not recorded</div><?php endif; ?>
      <?php if ($pmMissing && !$amMissing): ?><div class="partial">PM not recorded yet</div><?php endif; ?>
      <div class="entry-body">
        <div>AM &nbsp; <?= fmtBP($e['bp_am_sys'] ?? null, $e['bp_am_dia'] ?? null, $e['pulse_am'] ?? null) ?><?= !empty($e['bp_am_time']) ? ' <span style="color:var(--dim)">· ' . h($e['bp_am_time']) . '</span>' : '' ?></div>
        <div>PM &nbsp; <?= fmtBP($e['bp_pm_sys'] ?? null, $e['bp_pm_dia'] ?? null, $e['pulse_pm'] ?? null) ?><?= !empty($e['bp_pm_time']) ? ' <span style="color:var(--dim)">· ' . h($e['bp_pm_time']) . '</span>' : '' ?></div>
        <?php if (isset($e['ortho_sys']) && $e['ortho_sys'] !== null): ?><div>Standing <?= fmtBP($e['ortho_sys'], $e['ortho_dia'] ?? null, $e['ortho_pulse'] ?? null) ?></div><?php endif; ?>
        <div>Glucose <?= numOr($e['glucose_am'] ?? null) ?> &nbsp;·&nbsp; Weight <?= numOr($e['weight'] ?? null) ?><?= (isset($e['weight_white']) && $e['weight_white'] !== null) ? ' <span style="color:var(--dim)">(white ' . h($e['weight_white']) . ')</span>' : '' ?></div>
        <div>Grip R <?= $gr ? '<b>' . h(implode(' / ', $gr)) . '</b> kg' . (!empty($e['grip_time']) ? ' at ' . h($e['grip_time']) : '') : '—' ?></div>
        <?php if ($gl): ?><div>Grip L <b><?= h(implode(' / ', $gl)) ?></b> kg</div><?php endif; ?>
      </div>
      <?php if (!empty($e['notes_am'])): ?><div class="entry-notes"><b style="color:var(--dim)">AM:</b> <?= h($e['notes_am']) ?></div><?php endif; ?>
      <?php if (!empty($e['notes_pm'])): ?><div class="entry-notes"><b style="color:var(--dim)">PM:</b> <?= h($e['notes_pm']) ?></div><?php endif; ?>
      <?php if (empty($e['notes_am']) && empty($e['notes_pm']) && !empty($e['notes'])): ?><div class="entry-notes"><?= h($e['notes']) ?></div><?php endif; ?>
    </div>
  </div>
<?php endforeach; endif; ?>
</div>

</main>

<?php if ($exportPayload !== null): ?>
<script>
// one-shot: stream the just-exported row's payload as a download, built from the
// page's already-loaded data (no second server request), then scrub the query flag.
(function () {
  const payload = <?= json_encode($exportPayload, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
  const row = payload.row || {};
  const fname = 'curtis_vitals_export_' + (row.date || 'unknown') + '_id' + (row.id || 'x') + '.json';
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = fname;
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
  // scrub ?exported so a manual refresh won't re-download
  if (window.history && window.history.replaceState) {
    window.history.replaceState({}, '', '<?= REDIRECT_TO ?>');
  }
})();
</script>
<?php endif; ?>

<script>
"use strict";
const CONTRACT_VERSION = "<?= CONTRACT_VERSION ?>";
const $ = id => document.getElementById(id);

const NUM_FIELDS = <?= json_encode(NUM_FIELDS) ?>;
const TIME_FIELDS = <?= json_encode(TIME_FIELDS) ?>;
const EARPAIN_SYS = 178;

// ---- existing dates (for instant duplicate feedback; PHP is the real gate) ----
const EXISTING = {};
document.querySelectorAll('.entry[data-row]').forEach(el => {
  try { const j = JSON.parse(el.getAttribute('data-json')); EXISTING[j.date] = j.id; } catch (e) {}
});

function todayLocal() {
  const d = new Date(), p = n => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
}
function nowLocalTime() {
  const d = new Date(), p = n => String(n).padStart(2, '0');
  return p(d.getHours()) + ':' + p(d.getMinutes());
}
function stampNow(fieldId) {
  $(fieldId).value = nowLocalTime();
  if (fieldId === 'glucose_time' || fieldId === 'bp_am_time') updateInterval();
}
function setMode(m) {
  document.body.classList.toggle('mode-am', m === 'am');
  document.body.classList.toggle('mode-pm', m === 'pm');
  $('seg-am').classList.toggle('active', m === 'am');
  $('seg-pm').classList.toggle('active', m === 'pm');
}
function defaultMode() { return (new Date()).getHours() < 14 ? 'am' : 'pm'; }

function clearForm() {
  $('f_id').value = '';
  $('f_amend').value = '0';
  document.body.classList.remove('amending');
  $('date').value = todayLocal();
  NUM_FIELDS.concat(TIME_FIELDS, ['grip_1','grip_2','grip_3','grip_l1','grip_l2','grip_l3']).forEach(f => { if ($(f)) $(f).value = ''; });
  $('notes_am').value = ''; $('notes_pm').value = '';
  $('btn-add').textContent = 'Add';
  $('btn-cancel').classList.add('hidden');
  $('editing-flag').classList.add('hidden');
  $('date').classList.remove('dupe');
  updatePrompts(); updateInterval();
}

function loadRow(j, amend) {
  $('f_id').value = j.id;
  $('f_amend').value = amend ? '1' : '0';
  document.body.classList.toggle('amending', !!amend);
  $('date').value = j.date || '';
  NUM_FIELDS.forEach(f => { if ($(f)) $(f).value = (j[f] === null || j[f] === undefined) ? '' : j[f]; });
  TIME_FIELDS.forEach(f => { if ($(f)) $(f).value = j[f] || ''; });
  const g = Array.isArray(j.grip_r) ? j.grip_r : [null,null,null];
  ['grip_1','grip_2','grip_3'].forEach((id,i) => { $(id).value = (g[i]===null||g[i]===undefined)?'':g[i]; });
  const gl = Array.isArray(j.grip_l) ? j.grip_l : [null,null,null];
  ['grip_l1','grip_l2','grip_l3'].forEach((id,i) => { $(id).value = (gl[i]===null||gl[i]===undefined)?'':gl[i]; });
  $('notes_am').value = j.notes_am || j.notes || '';
  $('notes_pm').value = j.notes_pm || '';
  $('btn-add').textContent = amend ? 'Add (commit amendment)' : 'Update';
  $('btn-cancel').classList.remove('hidden');
  $('editing-flag').textContent = (amend ? 'Amending ' : 'Editing ') + (j.date||'') + ' (#' + j.id + ')';
  $('editing-flag').classList.remove('hidden');
  updatePrompts(); updateInterval();
  window.scrollTo({ top: 0, behavior: 'smooth' });
}

function updatePrompts() {
  const am = $('bp_am_sys').value, pm = $('bp_pm_sys').value;
  $('prompt-am').classList.toggle('show', am !== '' && Number(am) >= EARPAIN_SYS);
  $('prompt-pm').classList.toggle('show', pm !== '' && Number(pm) >= EARPAIN_SYS);
}
function minutesBetween(t1, t2) { const p = t => { const a = t.split(':').map(Number); return a[0]*60+a[1]; }; return p(t2)-p(t1); }
function updateInterval() {
  const el = $('interval');
  const gt = $('glucose_time').value.trim(), bt = $('bp_am_time').value.trim();
  if (!gt) { el.classList.add('hidden'); el.textContent=''; return; }
  if (bt) {
    let mins = minutesBetween(gt, bt); if (mins < 0) mins += 1440;
    const h = Math.floor(mins/60), m = mins%60;
    el.classList.remove('hidden');
    el.innerHTML = 'Meds→AM-BP interval: <b>~' + (h?h+'h ':'') + m + 'm</b>. Fong wants BP a couple hours post-meds — a wide gap makes this AM reading less comparable to others.';
  } else {
    el.classList.remove('hidden');
    el.innerHTML = 'AM meds (glucose time): <b>' + gt + '</b>. Fill the AM BP time to see the meds→BP interval.';
  }
}
function appendSx(label, targetId) {
  const ta = $(targetId), cur = ta.value;
  const prefix = (cur.length && !cur.endsWith('\n')) ? '\n' : '';
  ta.value = cur + prefix + label + ': ';
  ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.length;
}

// duplicate-date guard (client side; server re-checks and is the real gate)
function checkDupe() {
  const d = $('date').value, id = $('f_id').value;
  const clash = EXISTING[d] !== undefined && String(EXISTING[d]) !== String(id);
  $('date').classList.toggle('dupe', clash);
  return clash;
}
$('entry-form').addEventListener('submit', ev => {
  if ($('f_id').value === '' && EXISTING[$('date').value] !== undefined) {
    ev.preventDefault();
    $('date').classList.add('dupe');
    $('form-msg').textContent = 'DUPLICATE DATE (recommend editing existing)';
    $('form-msg').className = 'msg bad';
  }
});

// ---- wiring ----
$('seg-am').addEventListener('click', () => setMode('am'));
$('seg-pm').addEventListener('click', () => setMode('pm'));
$('btn-cancel').addEventListener('click', clearForm);
$('btn-clear').addEventListener('click', () => { clearForm(); $('form-msg').textContent=''; });
document.querySelectorAll('[data-sx]').forEach(b => b.addEventListener('click', () => appendSx(b.getAttribute('data-sx'), b.getAttribute('data-tgt'))));
document.querySelectorAll('[data-now]').forEach(b => b.addEventListener('click', () => stampNow(b.getAttribute('data-now'))));
['bp_am_sys','bp_pm_sys'].forEach(id => $(id).addEventListener('input', updatePrompts));
['glucose_time','bp_am_time'].forEach(id => $(id).addEventListener('input', updateInterval));
$('date').addEventListener('input', checkDupe);

// edit / amend: load the row's JSON into the form (client action, no server hit)
document.querySelectorAll('[data-editstart]').forEach(b => b.addEventListener('click', () => {
  const el = b.closest('.entry');
  try { loadRow(JSON.parse(el.getAttribute('data-json')), b.getAttribute('data-amend') === '1'); } catch (e) {}
}));

// collapse/expand rows (ignore clicks on buttons/forms inside the header)
document.querySelectorAll('.entry-head').forEach(h => h.addEventListener('click', ev => {
  if (ev.target.closest('button') || ev.target.closest('form')) return;
  h.parentElement.classList.toggle('open');
}));

// ---- drag-drop ack ----
const ackZone = $('ack-zone');
function readAck(file) {
  const r = new FileReader();
  r.onload = () => {
    let ack;
    try { ack = JSON.parse(r.result); } catch (e) { flashLocal('That file is not valid JSON — nothing changed.', true); return; }
    if (!ack || typeof ack !== 'object') { flashLocal('Ack file is not an object.', true); return; }
    if (ack.contract_version !== CONTRACT_VERSION) { flashLocal('CONTRACT MISMATCH: ack ' + (ack.contract_version||'?') + ' vs app ' + CONTRACT_VERSION + '. Nothing changed.', true); return; }
    if (ack.kind !== 'vitals_ack') { flashLocal('Not a vitals_ack file.', true); return; }
    if (!Array.isArray(ack.acked_ids) || !ack.acked_ids.length) { flashLocal('Ack has no acked_ids.', true); return; }
    // hand off to the server, which flips states + redirects
    $('ack_ver').value = ack.contract_version;
    $('ack_ids').value = ack.acked_ids.join(',');
    $('ack-form').submit();
  };
  r.onerror = () => flashLocal('Could not read the dropped file.', true);
  r.readAsText(file);
}
function flashLocal(text, bad) {
  let el = $('ack-flash');
  if (!el) { el = document.createElement('div'); el.id = 'ack-flash'; el.className = 'msg'; ackZone.after(el); }
  el.textContent = text; el.className = 'msg ' + (bad ? 'bad' : 'ok');
}
['dragover','dragenter'].forEach(t => window.addEventListener(t, e => { e.preventDefault(); ackZone.classList.add('drag'); }));
window.addEventListener('dragleave', e => { if (e.target === ackZone || e.target === document.body) ackZone.classList.remove('drag'); });
window.addEventListener('drop', e => { e.preventDefault(); ackZone.classList.remove('drag'); const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if (f) readAck(f); });

// ---- init ----
setMode(defaultMode());
clearForm();
</script>
</body>
</html>
