{
  "page": {
    "id": "bioce",
    "name": "BioCE",
    "url": "https://status.bioce.cloud",
    "time_zone": "Asia/Bangkok",
    "updated_at": "2026-08-27T22:25:57.803Z"
  },
  "components": [
    {
      "id": "bioce",
      "name": "bioce.cloud - estate landing",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 1,
      "description": "Static site served by nginx (vhost bioce.conf, server_name bioce.cloud + www.bioce.cloud). www answers 200 as well and is intentionally NOT a second component - same files, same vhost, same failure. If this is down, nginx itself is down and every other bioce.cloud row will be down with it.",
      "showcase": true,
      "start_date": null,
      "group_id": "public",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Course & public sites",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 184,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "course",
      "name": "course.bioce.cloud - course hub",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 2,
      "description": "Static hub + dashboard. This URL is the static half; the vhost also proxies /dashboard to 127.0.0.1:3100 (the same pgce-api process as the 'pgce-api' row), so a green 'course' with a red 'pgce-api' means the hub loads but the dashboard behind it will not.",
      "showcase": true,
      "start_date": null,
      "group_id": "public",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Course & public sites",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 187,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-dashboard.ps1"
    },
    {
      "id": "pgce",
      "name": "pgce.bioce.cloud - PG-CE LMS portal",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 3,
      "description": "The LMS single-page app: static files from nginx (vhost pgce.conf), so it stays 200 even when the API behind it is dead - which is precisely why 'pgce-api' is a separate component. pm2 is null on purpose: no process serves this URL.",
      "showcase": true,
      "start_date": null,
      "group_id": "public",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Course & public sites",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 200,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-pgce-lms.ps1"
    },
    {
      "id": "pgcs",
      "name": "pgcs.bioce.cloud - PG-CS site",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 4,
      "description": "Static landing page, nginx only.",
      "showcase": false,
      "start_date": null,
      "group_id": "public",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Course & public sites",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 209,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "trials",
      "name": "trials.bioce.cloud - clinical trials site",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 5,
      "description": "Static, nginx only. The sibling vhost clinical-trials.conf exists on the box but its hostname has no DNS record, so it is deliberately not a component here.",
      "showcase": false,
      "start_date": null,
      "group_id": "public",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Course & public sites",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 193,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "pgce-api",
      "name": "pgce.bioce.cloud/api - PG-CE LMS API",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 6,
      "description": "Fastify, pm2 app 'pgce-api' on 127.0.0.1:3100 under the OS user 'pgce' (PM2_HOME=/home/pgce/.pm2, systemd unit pm2-pgce.service). Answers {\"ok\":true,\"service\":\"pgce-api\"}. This is the backend for BOTH the LMS SPA and course.bioce.cloud/dashboard. The on-box guard rail probes 127.0.0.1:3100 directly and restarts this pm2 app after 3 consecutive local failures.",
      "showcase": true,
      "start_date": null,
      "group_id": "apps",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Apps & services",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.25,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 201,
      "last_http": 200,
      "pm2": "pgce-api",
      "port": 3100,
      "driver": "deploy-pgce-lms.ps1"
    },
    {
      "id": "consult",
      "name": "consult.bioce.cloud - BioCE Console",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 7,
      "description": "nginx reverse-proxy to the Cloudflare Worker bioce-console.bioce.workers.dev (cutover 2026-07-08). Probed at '/' because the Console has no /api/health route (it 404s there). pm2 is null: the OLD Fastify consult-api still runs on 127.0.0.1:3300 as the rollback, but it serves nothing on this hostname, so naming it here would annotate this row with an irrelevant process status. Pair this row with 'console-worker': consult red + console-worker green = nginx/the box; both red = the Console Worker. The guard rail never restarts nginx because this one row fails - the upstream is Cloudflare, not us.",
      "showcase": true,
      "start_date": null,
      "group_id": "apps",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Apps & services",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.07,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 390,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "webapps",
      "name": "webapps.bioce.cloud - clinical epidemiology apps hub",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 8,
      "description": "Static hub at /var/www/webapps served by nginx (vhost webapps.conf), plus /idiom2026/ proxied to 127.0.0.1:8003. RENAMED from webapp.bioce.cloud (singular) on 2026-08-27; TLS issued the same night (cert webapps.bioce.cloud, expires 2026-11-24) and the old name is now a path-preserving 301 to this one (vhost webapp.conf, backup of the pre-rename conf at /root/webapp.conf.before-rename-20260827 on the box). The old name is deliberately NOT a component: monitoring a redirect would only ever tell us nginx is up, which every other row already says. Verified at the swap: / 200, /idiom2026/ 200, /apps.json 200, and webapp.bioce.cloud/idiom2026/ -> 301 -> webapps.bioce.cloud/idiom2026/ (the path survives - that was the risk). This URL is the STATIC half: nginx serves the hub even when the app behind /idiom2026/ is dead, so a green row here does not prove the app is up. That app is NOT a pm2 process - it is the systemd unit idiom@03-MFP, so the pm2 field is null, the heartbeat's pm2 table can never annotate this row, and the guard rail does not restart it (out of the guard's declared blast radius; heal it by hand with `systemctl restart idiom@03-MFP`).",
      "showcase": false,
      "start_date": null,
      "group_id": "apps",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Apps & services",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 98.93,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 387,
      "last_http": 200,
      "pm2": null,
      "port": 8003,
      "driver": null
    },
    {
      "id": "herobioce",
      "name": "herobioce.uniqcret.com - HEROs CTMS",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 9,
      "description": "edge: cloudflare. Same VPS as everything else (pm2 'heros-api', cluster x2, 127.0.0.1:3000, OS user 'heros', PM2_HOME=/opt/heros/.pm2, systemd unit pm2-heros.service, nginx upstream heros_api in heros.conf) but its DNS is Cloudflare-PROXIED (orange cloud, resolves to 104.21.x), unlike every bioce.cloud host. kind stays 'http' because a failure here triages to THIS box; the DNS-vs-kind cross-check in watchdog.py only inspects non-http kinds, so it will not flag this row. It is also the only component on a different apex - it belongs to the HEROs lane and has its own alert-only monitor on the box (heros-health-check.sh); this watchdog does not replace it.",
      "showcase": false,
      "start_date": null,
      "group_id": "apps",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Apps & services",
      "kind": "http",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 382,
      "last_http": 200,
      "pm2": "heros-api",
      "port": 3000,
      "driver": null
    },
    {
      "id": "console-worker",
      "name": "bioce-console.bioce.workers.dev - Console Worker",
      "status": "operational",
      "created_at": "2026-08-27T20:58:56.208Z",
      "updated_at": "2026-08-27T22:25:56.203Z",
      "position": 10,
      "description": "The Cloudflare Worker behind consult.bioce.cloud, probed directly so an outage can be split between Cloudflare and our nginx. kind 'external' means watchdog.py will not count it as a box failure - correct, nothing on the VPS can fix it.",
      "showcase": false,
      "start_date": null,
      "group_id": "infra",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Infrastructure",
      "kind": "external",
      "flapping": false,
      "uptime_90d": 99.29,
      "last_check_at": "2026-08-27T22:25:56.203Z",
      "latency_ms": 215,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "vps",
      "name": "VPS HERO (72.60.74.175) - heartbeat",
      "status": "operational",
      "created_at": "2026-08-27T22:25:57.803Z",
      "updated_at": "2026-08-27T22:25:55.000Z",
      "position": 11,
      "description": "Virtual component: no probe. /opt/bioce-watchdog/heartbeat.sh POSTs {source:'heartbeat', ...} to the Worker every minute from root's cron; the Worker turns it red when nothing has arrived for policy.heartbeat_stale_minutes (3). A watchdog on the box could never report the box being dead, so the box only pushes and staleness is judged off-box. The payload also carries the merged pm2 table for BOTH users (pgce + heros), nginx's active state, uptime, load1, disk %, memory and the guard rail's last heal.",
      "showcase": true,
      "start_date": null,
      "group_id": "infra",
      "page_id": "bioce",
      "group": false,
      "only_show_if_degraded": false,
      "group_name": "Infrastructure",
      "kind": "heartbeat",
      "flapping": false,
      "uptime_90d": 100,
      "last_check_at": "2026-08-27T22:25:55.000Z",
      "latency_ms": null,
      "last_http": null,
      "pm2": null,
      "port": null,
      "driver": null
    }
  ],
  "incidents": [],
  "scheduled_maintenances": [],
  "status": {
    "indicator": "none",
    "description": "All Systems Operational"
  }
}