# Public sharing, reports, subscriptions and Cachet migration Every address below has a page variant under `/status/{slug}`. Paths are generated from the selected page and configured installation URL, including a hosting base folder. A published page is required for every public response. Archived pages, foreign IDs, disabled components and components in hidden groups are unavailable. * `/badges/components/{id}.svg` and `/badges/groups/{id}.svg`: current escaped SVG status, with a link to the public page. Cache lifetime: 30 seconds. * `/feed.xml`: RSS 2.0 with up to 50 public incidents and 50 maintenance windows, canonical incident links and XML escaping. Cache lifetime: 60 seconds. * `/incidents/{id}`: public incident timeline, safe Markdown, no internal incident or hidden-component disclosure. * `/embed.js`: copy its script tag from **Monthly reports → Share your status**. It inserts a linked status label after the script, fetches only public `/widget.json` without cookies, refreshes each minute and shows an unavailable state if the request fails. JSON and script allow public CORS; DOM text is rendered with `textContent`, never `innerHTML`. ## Service subscriptions The sign-up form defaults to all services, including future services. To receive only selected components, uncheck All services and select one or more components. Confirmation is still by signed email link. Existing subscribers retain their all-service behavior. Public sign-up cannot change an already active subscriber. The confirmation page and notification email link to service preferences. This uses a signed relative URL and the subscriber's current token. Both are required; a rotated token invalidates earlier links. The existing long-lived unsubscribe URLs remain compatible. Preference forms also require CSRF protection. Incidents and maintenance are filtered by the selected components, both when queued and before delivery. Subscribers already told about an incident keep its updates and resolution even if its component associations change. Changing service preferences invalidates that earlier selection history, so queued irrelevant mail is skipped. General notices without component associations go to all subscribers. ## Monthly uptime reports `/reports`, `/reports.csv`, `/reports.pdf` provide public reports. The matching `/admin/reports*` routes also include this page's hidden or disabled components, and require current page access. Read-only page viewers can download reports. Choose `?month=YYYY-MM`, January 2000 through the current month. Reports use **UTC calendar months** because daily rollups cannot establish exact local intraday boundaries. Do not interpret them as a local-time SLA. Percentages are weighted by observed up/down seconds. Missing data is unobserved, never assumed available. Coverage compares observed seconds to elapsed month seconds minus actual recorded maintenance windows; overlapping windows are counted once. New check rollups exclude only the maintenance portion of the capped observed UTC interval. Scheduled-but-unstarted work does not count as an exclusion; cancellation ends the exclusion. Raw check history and old daily totals remain intact. Historical daily totals recorded before this accounting change may still include maintenance observations; reports/export metadata state this limitation rather than fabricating a retrospective correction. Manually set maintenance states and delayed/missing monitoring samples may appear as unobserved time. The current month stops at the present time. CSV cells are spreadsheet-formula escaped. PDF is rendered by Dompdf with Unicode fonts, remote loading, PHP and JavaScript disabled and an escaped controlled report template. Reports are bounded to 500 components and 2000 maintenance windows per page per month. Reports reject pages/months above those limits instead of silently omitting records. Split large pages before generating a report. ## Scoped API and Prometheus Legacy `/api/v1/components` and `/api/v1/incidents` contracts remain. New routes also exist under `/api/v1/pages/{slug}`. Authenticate using `Authorization: Bearer TOKEN` or `X-Cachet-Token`. A read token can read; mutations require a write token and the owner's **current** page editor/admin rights. Revoking page membership or lowering the role immediately removes the corresponding token access. * `GET /ping`: Cachet-style `{"data":"Pong!"}` on published pages. * `/groups`: GET list, POST create; `/{group}` GET, PUT and DELETE. Fields: `name`, `visible`, `collapsed`, `position`. * `POST /components`: create using `name`, optional `description`, `status` (1–5), `group_id`, `enabled`, `show_uptime`, `position`. DELETE `/{component}` removes a component. Existing PUT/POST status updates continue to work. * DELETE `/incidents/{incident}`. Existing create and timeline-update routes remain. * `/subscribers`: GET (paginated, default 50, maximum 100), POST creates a pending unconfirmed subscriber; `/{subscriber}` GET, PUT service preferences and DELETE. Subscriber routes additionally require the token owner to administer this page. Tokens and signup IPs are never returned. Subscriber creation sends no email; resend confirmation from the subscriber administration screen. * `/maintenance`: GET list, POST create; `/{maintenance}` GET, PUT and DELETE. Times require ISO 8601 offsets (`2026-10-09T20:00:00+02:00`), end after start, and bounded component IDs belonging to this page. PUT edits only scheduled windows; deleting running work restores held component states before removal. Announcement minutes follow the existing allowed lead times. Private new read endpoints also work on unpublished pages, with a valid scoped read token. Archived pages are unavailable. New API request bodies are capped at 256 KiB before native request capture decodes JSON, including requests without Content-Length, encoded route prefixes and trailing slashes. JSON POSTs to legacy component/incident ID paths use this same limit because a body `_method` can select the new DELETE route; ordinary update verbs, fields and authority remain compatible. Other legacy API paths retain their existing limits. The shared native guard also bounds probe request bodies to16 KiB before capture; globally prepended middleware retains these checks for alternate Kernel entry points. Resource arrays and lists are bounded and per-minute throttles apply. Prometheus: authenticated `GET /metrics` (or `/api/v1/metrics`) exposes gauges `pharos_component_status` (1–5) and `pharos_component_uptime_ratio` (observed up fraction over 30 UTC days; `NaN` with no observations). Only ID and escaped name labels are emitted, never probe targets, check messages or credentials. Up to 5000 enabled components are included. Use the central installation hostname; custom public-page domains do not accept API credentials. Page variants: `/status/{slug}/metrics` and `/api/v1/pages/{slug}/metrics`. ## Cachet 2.x importer Open **Import Cachet 2.x** in the selected page's administration (requires page admin), upload JSON, review the named resources/counts, then import. The preview expires after 20 minutes, is encrypted in server cache, belongs to the current user and selected page and is consumed once. Revoked page rights block application. Application validates again and runs in one database transaction. Identical normalized exports cannot be applied twice to the same page. Resource order, object key order, equivalent UTC timestamps, explicit default fields and ignored metadata do not create a new import identity. Component/group source references remain significant; incident/subscriber IDs with no destination use are ignored. Accepts one local JSON object with `groups` (alias `component_groups`), `components`, `incidents`, `subscribers`. Each value is either a list or a Cachet API envelope `{"data":[...]}`. Combine complete, paginated Cachet API resources into those lists before uploading; Pharos does not fetch arbitrary URLs or remote credentials. Maximum: 4 MiB, 1000 rows per resource, 100 updates per incident. A minimal export: ```json { "groups": [{"id": 1, "name": "Website", "visible": 1}], "components": [{"id": 10, "name": "API", "group_id": 1, "status": 1}], "incidents": [{"id": 20, "name": "Past outage", "component_id": 10, "status": 4, "visible": 1, "message": "Recovered", "occurred_at": "2026-09-01T12:00:00Z", "updated_at": "2026-09-01T13:00:00Z", "updates": [{"status": 2, "message": "Cause found", "created_at": "2026-09-01T12:30:00Z"}]}], "subscribers": [{"id": 30, "email": "reader@example.com", "global": false, "verified_at": "2026-09-01", "subscriptions": [{"component_id": 10}]}] } ``` IDs are source references, mapped to newly created destination IDs. Referenced groups/components must be present in this export; no destination-page IDs are accepted as source relations. Incident visibility and group visibility are preserved; absent visibility defaults private. Dates without offsets mean UTC. Verified dates preserve subscriber consent. Existing destination subscribers are left intact, including opt-outs and preferences. Source verification codes, credentials and unrecognized metadata are discarded. No mail, webhook or confirmation storm is generated. This adds history; it never overwrites existing groups, components or incidents and does not import checks/settings/users. CLI uses a local file and defaults to dry run: ```sh php artisan pharos:cachet-import /absolute/path/export.json --page=2 php artisan pharos:cachet-import /absolute/path/export.json --page=2 --apply ``` Technical references: [Cachet 2.4 models](https://github.com/CachetHQ/Cachet/tree/2.4/app/Models), [Dompdf](https://github.com/dompdf/dompdf), [Prometheus text exposition](https://prometheus.io/docs/instrumenting/exposition_formats/). Development evidence includes PHP feature tests plus native Node execution of the widget DOM and `pdftotext` extraction of a real Unicode report. These tools are used for verification, not at runtime by Pharos.