{
  "page": {
    "id": "bioce",
    "name": "BioCE",
    "url": "https://status.bioce.cloud",
    "time_zone": "Asia/Bangkok",
    "updated_at": "2026-10-25T11:05:47.446Z"
  },
  "components": [
    {
      "id": "bioce",
      "name": "bioce.cloud - estate landing",
      "status": "operational",
      "created_at": "2026-10-18T12:41:42.834Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.68,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 219,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "course",
      "name": "https://bioce.cloud/course - course",
      "status": "operational",
      "created_at": "2026-10-18T11:28:42.631Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 2,
      "description": "The course front page: nginx `location = /course` (bioce-programmes.conf, generated by central-routing.py) serves /var/www/course/index.html, and that file is shipped by the router lane - deploy-bioce-router.ps1, whose central-routing.py sources include 'Course website/course/index.html'. That is this row's driver. Until T-168 it said deploy-dashboard.ps1, which was wrong: that script ships ONLY /var/www/course/dashboard (the hub SPA at bioce.cloud/dashboard) and never touches the file this URL serves. /dashboard is not probed by this row; it is a static SPA gated by auth_request to 127.0.0.1:3100 (the 'pgce-api' process), so a green 'course' with a red 'pgce-api' means this page loads but the hub behind the sign-in 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.67,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 429,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    },
    {
      "id": "pgce",
      "name": "https://bioce.cloud/app/ - pgce",
      "status": "operational",
      "created_at": "2026-10-18T12:42:42.605Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.7,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 438,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-pgce-lms.ps1"
    },
    {
      "id": "pgcs",
      "name": "https://bioce.cloud/pgcs - pgcs",
      "status": "operational",
      "created_at": "2026-10-18T12:41:42.834Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.66,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 438,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "trials",
      "name": "trials.bioce.cloud - clinical trials site",
      "status": "operational",
      "created_at": "2026-10-16T23:01:39.484Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.69,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 221,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "pgce-api",
      "name": "https://bioce.cloud/api/health - pgce-api",
      "status": "operational",
      "created_at": "2026-10-23T02:17:17.800Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.67,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 429,
      "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-10-22T01:10:42.595Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.2,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 504,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "webapps",
      "name": "webapps.bioce.cloud - clinical epidemiology apps hub",
      "status": "operational",
      "created_at": "2026-10-18T12:41:42.834Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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": 99.64,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 437,
      "last_http": 200,
      "pm2": null,
      "port": 8003,
      "driver": null
    },
    {
      "id": "herobioce",
      "name": "herobioce.uniqcret.com - HEROs CTMS",
      "status": "operational",
      "created_at": "2026-10-23T02:24:18.724Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.66,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 647,
      "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-10-16T23:01:39.484Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "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.78,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 438,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "vps",
      "name": "VPS HERO (72.60.74.175) - heartbeat",
      "status": "operational",
      "created_at": "2026-10-16T23:02:37.000Z",
      "updated_at": "2026-10-25T11:05:16.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": 99.96,
      "last_check_at": "2026-10-25T11:05:16.000Z",
      "latency_ms": null,
      "last_http": null,
      "pm2": null,
      "port": null,
      "driver": null
    },
    {
      "id": "bioce-webapp",
      "name": "bioce.cloud/webapp - apex apps catalogue",
      "status": "operational",
      "created_at": "2026-10-18T12:42:42.605Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 12,
      "description": "Apex-native catalogue (T-64). Must stay 200 with a different body from the landing. Never proxies /idiom2026/.",
      "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.38,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 654,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    },
    {
      "id": "bioce-real-404",
      "name": "bioce.cloud nonsense path (proves real 404)",
      "status": "operational",
      "created_at": "2026-10-18T12:41:42.834Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 13,
      "description": "T-64 acceptance: unknown apex paths must 404.",
      "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.38,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 654,
      "last_http": 404,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    },
    {
      "id": "course-pgce-docs",
      "name": "bioce.cloud/pgce/2026/docs - gated notes",
      "status": "operational",
      "created_at": "2026-10-23T02:17:17.800Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 14,
      "description": "PERMANENT. deploy-course.ps1 replaces /var/www/course and DELETES the gated editions. Cookie-less expect 302.",
      "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.33,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 656,
      "last_http": 302,
      "pm2": null,
      "port": null,
      "driver": "deploy-docs.ps1"
    },
    {
      "id": "legacy-api-closed",
      "name": "pgce.bioce.cloud/api - retired-host API (closing)",
      "status": "operational",
      "created_at": "2026-10-18T12:41:42.834Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 15,
      "description": "T-168 TRANSITION row (083 one door). The old LMS host's API, after the owner's 2026-09-23 ruling to close it. BEFORE the Release 2 router deploy, nginx still proxies /api/* on the old hosts to 127.0.0.1:3100, so this answers 200 and is backend-dependent (it goes red together with 'pgce-api'). AFTER it, nginx answers 410 JSON HOST_RETIRED on both schemes with no proxy - deliberately not a redirect, because a stale tab's fetch would fail CORS with an unreadable error. Both are green; 308/301 (a redirect crept back), 404/401 and every 5xx are red. pm2 is null on purpose: after R2 no process serves this URL, and before it the process is already the 'pgce-api' row's. The legacy vhost does NOT include the maintenance snippet, so this row can never carry the marker: until R2 ships, stopping pgce-api during `maintenance on` turns it red here. Release 3's admission tightens expect to [410].",
      "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.11,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 677,
      "last_http": 410,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    },
    {
      "id": "bioce-login-alias",
      "name": "bioce.cloud/login - sign-in alias",
      "status": "operational",
      "created_at": "2026-10-18T12:42:42.605Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 16,
      "description": "T-168 TRANSITION row (083 one door). BEFORE the Release 2 router deploy the apex has no /login and its real-404 catch-all answers 404; AFTER it, `location = /login` (and /login/) answers 302 to /app/login. The probe never follows redirects, so the 302 itself is the result. Both are green; 200 (the old soft-404 landing is back), 301/308 (a permanent redirect browsers would cache) and every 5xx are red. Apex path, so the maintenance marker applies. Release 3's admission tightens expect to [302].",
      "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.22,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 720,
      "last_http": 302,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    },
    {
      "id": "bioce-shell-asset",
      "name": "bioce.cloud shared shell script",
      "status": "operational",
      "created_at": "2026-10-18T12:42:42.605Z",
      "updated_at": "2026-10-25T11:05:20.506Z",
      "position": 17,
      "description": "T-168 TRANSITION row (083 one door). The one shared header every apex page loads (Bioce landing/assets/bioce-shell/shell.js, served no-cache). BEFORE the Release 2 router deploy it does not exist yet and /assets/ answers 404; AFTER it, 200. Both are green; 302/401/403 (the shell went behind a login wall anonymous pages cannot cross) and every 5xx are red. Apex path, so the maintenance marker applies. Release 3's admission tightens expect to [200].",
      "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.09,
      "last_check_at": "2026-10-25T11:05:20.506Z",
      "latency_ms": 863,
      "last_http": 200,
      "pm2": null,
      "port": null,
      "driver": "deploy-bioce-router.ps1"
    }
  ],
  "incidents": [],
  "scheduled_maintenances": [],
  "status": {
    "indicator": "none",
    "description": "All Systems Operational"
  }
}