Monitoring API
Sign in at /account with Google to view your name, email, profile picture and sign-in provider, and manage separate read and write tokens. User ownership uses Google's stable subject ID, never an email address. The server generates 240-bit random credentials. SHA-256 hashes authenticate requests; an encrypted copy on the owner's metadata row enables later retrieval. Plaintext is never stored in the database.
Select Show token to retrieve a token, then copy or hide it. The browser clears the displayed value after one minute, on window blur, or when hidden. It does not write token values to browser storage. Active/revoked filters help find unused tokens. Delete asks for confirmation, then removes the metadata and authentication lookup together. Deleted tokens disappear from the list and return 401 on subsequent API use. Resources and history remain available.
Token management
These endpoints require the Google session. A Bearer token cannot manage or reveal tokens.
| Request | Result |
|---|---|
GET /api/tokens |
{ ownerId, tokens }; metadata includes canReveal, never the hash or ciphertext |
POST /api/tokens |
Creates a token from { name, scopes }; returns 201 with its initial value and metadata |
POST /api/tokens/{id} |
Explicit retrieval; returns { ownerId, id, token } for an active token owned by the session |
DELETE /api/tokens/{id} |
Permanently deletes active or revoked tokens; returns 204. Missing/unowned IDs return 404 |
POST/DELETE require exact same-origin JSON. Token IDs are UUIDs. All responses use Cache-Control: no-store. List/retrieve/delete share an owner budget of 30 requests per minute; creation allows 10 per minute. The browser sends X-Account-ID as an account consistency check; if present and different from the signed-in owner, the server returns 409 before accessing credentials. It never grants access based on this header.
Set TOKEN_ENCRYPTION_KEY to a random 32-byte base64 value, separate from AUTH_SECRET. Every server instance must use the same stable key. Keep a backup outside the database and never expose it through NEXT_PUBLIC_*. The implementation uses Node's authenticated encryption APIs with AES-256-GCM, a fresh 12-byte nonce, a 16-byte tag, and owner/token identity as authenticated data. Only the owner metadata row holds the encrypted copy; the hash lookup does not duplicate it.
Earlier hash-only tokens cannot be reversed into plaintext. Their next successful authenticated use captures an encrypted copy, without changing the credential or permissions. Invalid, wrong-scope, deleted or revoked tokens cannot trigger this upgrade. An unused older token whose original value is lost must be replaced. Revoked tokens cannot be revealed; their saved entry can be deleted.
Authentication continues by hash when retrieval is not configured. Token creation/retrieval requires a valid encryption key. After a key change, active credentials update their encrypted copies on successful use; unused copies need the original key or a replacement token. Coordinate key changes across instances. Concurrent deletion/revocation and recovery updates use conditional transactions, so recovery cannot recreate a removed credential.
External data endpoints require Authorization: Bearer <token> over HTTPS. Hook and provider-usage requests reject redirects and URLs containing userinfo, query strings, or fragments. VIBEMON_ALLOW_HTTP_LOCAL=1 permits only loopback HTTP for isolated development tests. The original /status, /api/status, /api/statuses, /api/memory, /api/metrics, and /api/stats payloads remain available with issued tokens. Replace user-chosen legacy tokens in existing hook configuration with a generated write token. App read tokens and hook write tokens may differ but must belong to the same Google account.
Setup
Set AUTH_URL to the public HTTPS origin, AUTH_SECRET to a cryptographically random secret of at least 32 bytes, and AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET to the Google OAuth web application's credentials. Register ${AUTH_URL}/api/auth/callback/google as an authorized redirect URI. Local development allows http://localhost:3030. Never prefix these variables with NEXT_PUBLIC_.
The existing DynamoDB table must have TTL enabled on ttl. The runtime needs GetItem, PutItem, UpdateItem, DeleteItem, and Query access; transactions authorize against their constituent operations. Existing rows without ttl are preserved. Set DYNAMODB_ENDPOINT only for an isolated local database. No production resources are changed by the verification script.
Collect a summary
POST /api/v1/ingest requires write. Maximum JSON payload: 16 KiB.
{
"sourceId": "spark-1",
"kind": "host",
"displayName": "DGX Spark",
"observedAt": "2026-10-09T12:00:30.000Z",
"health": "ok",
"heartbeat": false,
"memoryType": "unified",
"metrics": { "cpuPercent": 24, "memoryPercent": 51, "gpuPercent": null, "gpuTemperatureC": 48 },
"maximums": { "cpuPercent": 29, "memoryPercent": 52, "gpuPercent": null, "gpuTemperatureC": 49 }
}
sourceId: 1–128 ASCII letters, digits,_,.,:,-; starts with a letter or digit. Unique within the owner. Agent identity cannot become a resource or change accounts. Resource collection presets can change after the owner saves resource settings.kind:agent,resource,host, orcluster.displayName: 1–128 characters.hostandclusterare built-in presets;resourcesupports user-defined types and measurements.observedAt: UTC ISO timestamp, no older than 120 seconds and no more than 30 seconds into the future. Use the end of the observed window.health:ok,warning,error, orunknown. Missing or failed measurements arenull, never zero.- Preset
metrics: finite percentages from 0 to 100; GPU temperature from 0 to 150 °C; node and pod summary counts from 0 to 1,000,000. Unknown fields are rejected. Omitted metric values becomenull. - Host/cluster metrics:
cpuPercent,memoryPercent,gpuPercent,gpuTemperatureC,nodesReady,nodesTotal,podsUnhealthy. - Agent metrics:
contextPercent,usageSessionPercent,usageWeekPercent. An agent also suppliesagent: { state, character, project, tool?, model? }using the existing state registry. Project labels are at most 128 characters; state, character, tool, model, and model-usage labels are at most 64. Unknown characters use the default character. maximums: optional window maxima with the same supported fields asmetrics. Defaults to the supplied summary values. A maximum cannot be below its average.memoryType:ram,unified, ornull.heartbeat: defaults tofalse. Heartbeats update the latest state and receipt time but do not add trend samples.
Success: { "accepted": true }. Equal or older observation timestamps return { "accepted": false, "reason": "duplicate_or_older" }; they do not refresh receipt time. The response remains HTTP 200, so a sender can discard obsolete work. A lost connection must not create a history queue: send the newest observation after reconnecting.
Custom resources
Open /resources/new from Custom resource on the dashboard or connection page. Enter the source ID, display name, arbitrary resource type, and measurements. Generate definition validates the form; Download definition saves the reusable JSON. Creating a definition does not register a connected resource. A collector registers it by sending its first authenticated observation.
Use this contract for databases, queues, HTTP services, storage, sensors, or any other resource. No type registration or client code change is required. The built-in Python collector supplies host/cluster presets; custom collectors supply their own measurements.
{
"sourceId": "freezer-1",
"kind": "resource",
"resourceType": "Cold storage",
"displayName": "Warehouse freezer",
"statusMessage": "Cooling",
"observedAt": "2026-10-09T12:00:30.000Z",
"health": "ok",
"metricDefinitions": {
"temperature": { "label": "Temperature", "unit": "°C", "display": "gauge", "min": -40, "max": 20 },
"power": { "label": "Power", "unit": "W", "display": "number", "min": 0 }
},
"metrics": { "temperature": -12, "power": null }
}
Set observedAt at measurement time. Send the definition with every observation to POST /api/v1/ingest using a write token. The generated page includes a downloadable definition and an observation template. Replace the timestamp placeholder at measurement time before sending it. Keep the whole request within 16 KiB.
resourceType: 1–64 characters, freely chosen. Change the saved resource settings before switching an existing collector's type.statusMessage: optional single-line text, up to 160 characters. It supplementshealth; do not send logs or secrets.metricDefinitions: 0–32 named definitions. Keys start with an ASCII letter and contain only letters, digits, or_, up to 64 characters.constructorandprototypeare reserved.- Each definition requires
label(1–80 characters),unit(0–16 characters), anddisplay(numberorgauge). Text rejects control characters. Use an empty unit for plain counts. minandmaxare optional bounds for numbers. Gauges require both, withmin < max. Values and bounds must be finite and within ±9,007,199,254,740,991. Negative numbers, fractions, and zero are supported within the declared bounds.metricsand optionalmaximumscontain only declared keys. Omitted or null values remain unavailable. Maxima must have the same null availability and cannot be below their averages.- For health/status-only resources, send
metricDefinitions: {}andmetrics: {}. Their cards omit numeric trends. metricOrder: optional array containing every declared key exactly once. It fixes the order across JSON/database serialization and both clients. When omitted, the server records the order received in the definition.- Without saved settings, collectors may change labels, display styles and ranges, or add keys. Changing units or removing keys requires an explicit settings update. With saved settings, a collector may update its measurement contract independently; settings are not overwritten by collection.
Metric selection is scoped to the resource ID. Different resources can use the same key with different units without sharing a display selection. Web and App render the declared label, unit, gauge range, and numeric history. Account metrics and character states remain independent.
Edit existing resources
Select Edit metrics on any resource card, including Spark and Kubernetes presets. /resources/{sourceId}/edit edits its name, type, metric labels, units, number/gauge display, ranges, and order. Add or remove rows; use the handles, arrow buttons, or Alt+Up/Down on a handle to reorder. The preview shows the layout without invented measurements. Save commits the configuration; Discard restores the last saved form. Removal can be undone before saving.
The owner configuration is stored separately from collector observations in the latest source row. Polling and new observations preserve it. Saving does not update receivedAt, clear stale state, or add a trend sample. Removing a displayed metric leaves retained measurements until normal expiry.
Values and history are matched by metric key and unit. New keys and changed units are unavailable until the collector supplies matching definitions; old values are never relabeled. Display-range changes retain measured values, including values outside the new visible gauge. Use the generated definition to update a custom collector when adding a measurement or changing its unit. A configured host/cluster can switch to kind=resource with the same source ID. Agent/resource identity boundaries remain fixed.
- Browser:
GET/PUT /api/resources/{sourceId}uses the Google session. PUT requires same-origin JSON. - External API:
GET/PUT /api/v1/sources/{sourceId}/settingsrequires a read/write token respectively. - GET returns
{ sourceId, configuration, measurementDefinitions }. Configuration containsdisplayName,resourceType,metricDefinitions,metricOrder, andrevision(0 before the first edit). - PUT sends the complete configuration and its last read revision. It returns the saved configuration with revision incremented. A stale editor receives 409; reload before saving again. Normal collector updates do not invalidate the editor's configuration revision.
- Payload limit: 16 KiB. Settings use the existing owner read/write budgets. Unknown or unowned sources return 404; agent settings return 400.
Latest source responses include configurationRevision. Configured presets also expose resourceType, metricDefinitions, and metricOrder, while retaining their collector kind. App preserves existing preset metric selections when these fields first appear.
Read latest state and trends
GET /api/v1/sources: all sources owned by the read token.GET /api/v1/sources?ids=spark-1,cluster-1: at most 100 selected IDs. Unknown IDs are omitted.GET /api/v1/sources/{sourceId}/trend?hours=1: minute aggregates. Allowed windows: 1 (default), 3, 6, 12, or 24 hours. Unknown or unowned sources return 404.
The list response is { sources: [...], serverTime }. Each source contains the observation, receivedAt (server receipt time), and stale. A source becomes stale 120 seconds after its last accepted receipt.
The trend response is { sourceId, points: [{ timestamp, samples, average, maximum, expiresAt }] }. Each minute averages received summary values per metric, excludes nulls, and keeps their maximum. samples counts accepted non-heartbeat summaries. Latest state and its aggregate commit in one DynamoDB transaction, preventing double counting under concurrent requests.
Custom and configured-resource trend responses also contain the current resourceType, metricDefinitions, and metricOrder. New metric keys are null in earlier minutes. Retained minutes preserve their measurement definitions internally: incompatible units are hidden rather than reinterpreted. A new incompatible collector sample resets the same-minute bucket. Minutes with no compatible numeric samples are omitted. Clients pair definitions and values from the same response.
Only latest state and minute aggregates are stored for this API. Expired aggregates disappear from reads after 24 hours, even while DynamoDB's asynchronous TTL deletion is pending. Physical TTL deletion follows DynamoDB's service schedule.
Limits and failures
Budgets are shared by all tokens of the same owner: 240 writes and 600 reads per minute. Token creation allows 10 requests per minute. Limits use shared DynamoDB counters, not per-process memory.
| HTTP status | Meaning | Client action |
|---|---|---|
| 400 | Invalid observation or query | Correct the input |
| 401 | Missing, unknown, or revoked token | Configure a current token |
| 403 | Wrong permission | Use the appropriate token |
| 409 | Source identity or metric-definition conflict | Keep the existing contract or use a new source/metric ID |
| 413 | JSON exceeds route limit | Send summary fields only |
| 429 | Request budget exhausted | Respect Retry-After |
| 503 | Storage, auth configuration, or contention failure | Retry with backoff and the latest summary |
Verification
Run pnpm lint, pnpm typecheck, and pnpm test. To verify actual database conditions and transactions, start an isolated DynamoDB Local on 127.0.0.1:18000, then run pnpm exec tsx scripts/verify-monitoring.ts. The script creates a randomly named test table and removes only that table on exit. It does not verify Google's consent screen or physical TTL deletion timing.
Multiple provider accounts
Google identifies the data owner. Each Codex, Claude, Kiro, or other agent login is a separate monitoring account within that owner. /coding-accounts/new generates account-specific launch settings. The first observation connects the account; the form does not log in to the provider or store its credentials. An agent observation can include:
{
"account": { "provider": "codex", "id": "work", "displayName": "Codex work" },
"sourceId": "agent:workstation-project",
"agent": { "state": "working", "character": "codex", "project": "my-project" }
}
account.id uses the same identifier rules as sourceId. provider is a lowercase identifier of 1–32 letters, digits, _, or -, beginning with a letter. Keys such as codex:work, codex:personal, claude:work, claude:personal, kiro:work, and kiro:personal are independent, even with identical project labels. Providers are not restricted to a fixed list. A registered agent source cannot change its kind or account. Display names may change.
The original /api/status also accepts optional account, sourceId, and observedAt fields. Payloads without account metadata use the character's provider and account ID default. Newly installed hooks include a source ID derived from provider, account, machine, workspace, and project; equal project names no longer collide. Legacy requests without a source ID derive one from provider, account, and normalized project.
GET /api/v1/accounts: account summaries and their member source IDs.GET /api/v1/accounts/{provider:id}: one account's latest summary.GET /api/v1/accounts/{provider:id}/trend?hours=1: per-minute means and maxima across that account's sources. Usage percentages are averaged, never added together.GET /api/v1/sources?accounts=codex:work,claude:personal&ids=spark-1: union of selected accounts and explicit source IDs. Every selection remains restricted to the token owner.GET /api/v1/sources/{sourceId}: one source.DELETE /api/v1/sources/{sourceId}: remove one source with a write token. The project-only legacy delete returns 409 when several accounts have that project, preventing an ambiguous deletion.
Use VIBEMON_ACCOUNT_ID and VIBEMON_ACCOUNT_NAME in each coding agent's launch environment. Provider-specific variables such as VIBEMON_CLAUDE_ACCOUNT_ID, VIBEMON_CODEX_ACCOUNT_ID, VIBEMON_KIRO_ACCOUNT_ID and matching _NAME variables override common values. Set VIBEMON_INSTANCE_ID when a stable machine label is needed; the default is the hostname. IDs and names are metadata, not provider credentials. Launch each tool in its matching signed-in environment; metadata does not switch provider logins. Unsupported usage windows remain null.
Use the corresponding CLAUDE_CONFIG_DIR or CODEX_HOME for that account's login. Named accounts have separate usage and context-cache keys and never borrow the default account's cache. Context keys also include the resolved workspace path, so repositories with the same basename cannot share context values. Older project-only cache entries are ignored until statusline writes a fresh scoped entry. A custom Claude config directory does not read the default macOS Keychain entry. If that profile stores credentials in a custom service, set CLAUDE_KEYCHAIN_SERVICE explicitly; otherwise the profile credentials file or the profile's CLI usage command must provide usage.
The legacy activity API now derives hour/day/week views from retained minute samples. Requests for older ranges remain syntactically valid, but only the last 24 hours are available. No raw status log aggregation or duplicate STATUS rows are written. Idle/sleep presentation is derived from observation age without changing server receipt time.
Browser dashboard
Sign in from / to open /dashboard. The same Google session opens /account for token management and /docs for setup (/connect redirects there). Web users do not copy an API token to view their own data. /agents remains available for the legacy token-based agent view.
GET /api/dashboard is the browser-only session adapter. It accepts the same optional ids and accounts filters as the source API. ?view=source&id=...&hours=1 or ?view=account&id=...&hours=1 reads recent history. The owner is resolved only from the signed Google session. Browser dashboard, resource-settings, and token requests send X-Account-ID to bind the request to the account shown in the page. A changed session returns 409 before reading or writing data; the header never grants access. Account changes discard the previous account's editor and selection state. The dashboard endpoint shares the owner's 600-read/minute budget and sends Cache-Control: no-store. It does not accept ingestion or other mutations; external /api/v1/* clients still require scoped Bearer tokens.
Latest data refreshes every 30 seconds while visible. An open trend refreshes every minute. Closing the panel, removing its selection, or hiding the page stops trend requests. Resource/account and metric controls affect the displayed cards and plots. Failed collection, unsupported values, stale receipts, empty data, and authentication/network failures remain distinct.