CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# Changelog
All notable changes to this project are documented in this file. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Alert configuration, from zero to paged in four steps.** A new screen
says where the alerting stands and gets a workspace live: decide who gets
paged (one click makes a published path for you, a colleague or a schedule
and names it on the route that catches everything), connect a source from a
grid of tools, receive a first alert (the source page waits for it live),
check that an alert paged someone. Each source has its page: setup steps
and a curl command, the secret rotated and shown once, the payload of the
last alerts, the attribute mapping edited against a real sample with a
value preview and suggestions, the priority rule, the ingest filter, and a
tester that says what any payload would do — route, who it pages, the
incident — before sending it as a test or for real. An attributes page
owns the vocabulary, with a coverage column. Routes are edited on a page of
their own — sources, conditions, stackable paging rules with one-click
paths, incident template, grouping, Slack channel, options — and previewed
against the last alerts before saving; the list shows their order and
reorders it. Priorities take aliases and a default. Alerts get a search box
and notes; the alert's history names who a route paged and why a rule was
skipped. The team's escalation path is picked from a list in the catalog.
- **The post-mortem is a document now.** Three columns: the contents and the
checklists on the left, the document in the centre — its own title, the
incident's metadata and metrics, sections written in Markdown with a toolbar
and a live preview, blocks that insert the timeline, the follow-ups or the
impact metrics where the cursor is, comments under each section — and on the
right the history of every change, with restore. Sections are renamed,
reordered, added and removed; a workspace edits its template in the
post-incident settings, hints included. The assistant still works one section
at a time — regenerate, tighten, enrich with facts, rewrite — and a new
**Check against the facts** reads the whole document once and marks where a
section misses a fact or contradicts the timeline, without touching a word.
Export as Markdown (file or clipboard), open as a printable page, read it
through `GET /api/v1/incidents/{ref}/post-mortem`; a **Post-mortems** page
lists every document with its status, owner and what is still open under it.
- **Root cause analysis (RCA), the agentic way.** A live incident now gets an
assessment the moment it is declared: the assistant gathers the evidence —
timeline, linked alerts, changes recorded around it, similar past incidents
with the cause their post-mortem documented, runbooks, ownership, what
responders said — states findings that must cite it, proposes hypotheses of
what broke and why with an explicit confidence on five levels, and has a
second, adversarial pass try to break them before anyone reads them. One
synthesis (what is going on, what caused it, what to do next) lands in the
incident's Slack channel and is updated in place; the timeline records each
assessment. A person steers it with notes, pauses it, grades it against the
documented cause, and sends the surviving hypothesis into the post-mortem's
RCA section as a draft. It re-assesses when a new signal lands and never
executes anything. Enterprise edition (`aiInvestigations`), its own switch in
AI governance, `GET`/`POST /api/v1/incidents/{ref}/investigation`.
### Changed
- **SMS and voice are two operators, not one.** A new `@openincident/notify`
package holds the transports, and the two channels are resolved separately:
the texts go through Brevo as soon as `BREVO_SMS_SENDER` is set — it reuses
the key the mail already uses and costs less per message — and fall back to
Twilio, while the calls stay on Twilio, the one operator of the shortlist
that hands the keypress back so an escalation can be acknowledged. An
instance can therefore text without being able to call: the phone field
offers only SMS, and a dead row says which operator is missing. A call
without a token to come back to no longer says "press 4", because nothing
was listening for the 4.
- **Routing no longer needs the catalog.** A route names who to page directly —
an escalation path, or a path found from an alert attribute through the
catalog when a workspace has one, with a fallback — and stacks several rules,
each with its own conditions. Conditions replace the three filter lines:
groups ORed, conditions ANDed, with contains, matches, one of, missing.
Routes choose their sources, carry an incident template (type, start in
triage or active, severity from the priority or fixed, private, custom fields
from attributes, decline the triage incident when the alert resolves), a
grouping rule (attributes, fixed or extending window, page again on every
alert or on a priority rise, a grace period) and a Slack channel to notify.
Alert attributes are a registry the workspace owns — text, list, priority or
a catalog type, required or not, with a merge strategy for repeats — and every
source maps its payload onto it, with light transforms and a match guard. A
source decides the alert's priority, statically or from a payload field, and
filters what it ingests; priorities carry aliases and a default. Every new
workspace starts with P1–P3, the five usual attributes and one route that
catches everything and opens triage incidents — name who to page and it is
live. Existing routes were converted; the team's escalation path became a
validated reference; the pipeline now decides first and applies second, so a
payload can be previewed without being ingested.
- **The product now says "root cause analysis (RCA)".** The post-mortem's
_Root cause_ section is _Root cause analysis (RCA)_ in every language, the
guide and the glossary explain the term, and the new analysis is named after
it — the words the rest of the industry uses.
- **Integrations screen: the whole catalog, with marks.** Seven families
instead of five — alert sources, chat and video, trackers, documentation,
identity, catalog and infrastructure as code, migration and business — and
fourteen cards that were missing, including four already shipped and simply
not shown: the Google, Microsoft and GitHub sign-in providers, Backstage
imports, the catalog importer and SCIM provisioning. Every tile now carries a
drawn mark rather than two letters, and enterprise sign-on stopped announcing
itself as a future milestone: it ships.
- **`@openincident/api-spec`,** the OpenAPI document as a package. The route
serves it, the developer site renders its reference from it, and a test
compares it with the routes `apps/web` really exposes — in both directions.
Documentation can no longer drift from the product without failing the build.
## [0.0.3-alpha] - 2026-09-05
### Added
- **A certificate gate for custom status-page domains** (`GET /api/tls`). Status
pages answer on the customer's own domain, and that domain is only known at
the first TLS handshake, so the proxy has to be allowed to request a
certificate for a hostname it has never seen. Doing that without a filter
lets anyone pointing DNS at the server mint certificates in the instance's
name; the endpoint answers 200 only for a domain a published page actually
carries, 404 for the rest. `STATUS_TLS_ASK_KEY` restricts who may ask. The
install chapter shows how to wire it as a proxy's `ask` URL.
### Changed
- **Sign-in sends an already signed-in member straight in.** The session is
global and its cookie can be set on the parent domain, so landing on a
workspace's sign-in page with a valid session and a membership is common;
the page asked for a password again, which nobody can answer differently.
## [0.0.2-alpha] - 2026-09-05
The seam a control plane plugs into. The product still sells nothing and keeps
no card: in the cloud edition it asks its control plane what to show.
### Added
- **Settings → Subscription & invoices (cloud edition).** The workspace's plan
and trial, the seats it covers, this month's usage against the plan's
ceilings, the public offers as the control plane sells them (monthly or
yearly, seat stepper), the invoice history it mirrors from the payment
provider, and the checkout, customer portal and "re-check now" actions —
every one of them a redirect to a session the control plane opens. Owners
act; other managers read. A paused workspace keeps this one screen reachable
so the owner can subscribe again. Reached through `CLOUD_GATEWAY_URL` and
`CLOUD_GATEWAY_SECRET`; without them the screen says the instance has no
control plane, and a self-hosted instance does not have the screen at all.
- **`purgeWorkspace` exported by `@openincident/db`**, so a control plane can
run the very same verified purge the `workspace:purge` command runs.
## [0.0.1-alpha] - 2026-09-05
The first public release: the whole product as it stands after its first
milestones — incident response end to end, alerting and on-call with a real
escalation engine, public and internal status pages served by a separate app,
reports, a governed assistant, Slack and Microsoft Teams, trackers and
documentation tools, a catalog you shape from the screen or from code, the
enterprise edition's single sign-on, SCIM provisioning and custom roles, a
user guide inside the product and a QA screen that runs the repository's own
suites. Alpha: the product works end to end and is covered by a smoke suite of
sixty-five journeys, but APIs, schema and screens still move, and no vendor
account (Slack, Teams, Twilio, S3, an inference provider) has been exercised
beyond mocks. Not production-ready yet — a very good time to try it and open
issues.
### Added
- **The foundation.** A pnpm + Turborepo monorepo with the web app, the worker
and the shared packages (database, auth, mail, config, crypto, UI tokens).
- **Real tenant isolation from day one.** Workspaces are resolved from the
`directory` schema before any tenant context is opened; every table of the
`app` schema carries `tenant_id` and is protected by row-level security that
the application role cannot bypass. An integration test tries to read across
two workspaces and expects nothing back.
- **Complete authentication.** Sign-in with email and password or Google,
Microsoft and GitHub; password reset; email invitations; email change with
confirmation; account deletion — nothing behind a dead link.
- **Response core schema.** Incidents, incident types, severities, statuses,
roles, timeline events, updates, participants, actions and follow-ups, plus
the catalog types the routing will lean on (Team, Service, Environment).
- **The demo workspace.** "Skylark Systems" and its reference incident INC-217,
replayable on an existing database.
- **Translations** in English (source), French and German, with dictionary
parity enforced at compile time.
- **Response, end to end.** Declaring an incident (live, retrospective or as a
drill), triage with accept, decline and merge, status updates with severity
and reminders, roles, a live timeline over server-sent events, follow-ups
and the post-incident flow with its two phases and tasks.
- **Public API v1.** Bearer keys created in the settings and shown once;
scopes `read`, `write` and `incident:create`; cursor pagination; a fixed
per-key rate limit; `{ error: { code, message } }` on every error; an
OpenAPI document served by the instance itself. The API and the web forms
share one write path, so an incident declared by an integration goes
through exactly the same rules.
- **Signed outbound webhooks.** Endpoints subscribe to incident events, every
delivery carries an HMAC-SHA256 signature, failures are retried, listed and
can be resent by hand; an endpoint failing for a week is disabled.
- **Living announcements.** Rules publish a post when an incident matches,
keep it updated at every status update and close it at resolution.
- **Settings.** Custom fields read by the declaration form, the post-incident
flow and the workspace's own word for its post-mortem, announcement
templates and rules, new incident types inheriting a base type.
- **Alert sources and routes.** One dedicated endpoint and secret per source
(Datadog, Prometheus/Alertmanager, Grafana, Sentry, CloudWatch, Uptime Kuma,
generic HTTP), payloads stored raw and parsed downstream, deduplication by
key and a five-minute grouping window; routes filter on attributes, escalate
statically or through the catalog (service → owner team → path), open an
incident never, always or conditionally in triage, and can run in test mode.
- **Escalation paths.** Versioned graphs of levels, conditions (working hours,
priority, urgency), delays, retries and hand-overs, edited as a draft and
published; a persisted state machine advances them tick by tick with
idempotent transitions, retries within a level, acknowledgement from the
web, a one-tap link, or a voice call; a dry run names who would be paged.
- **Schedules.** Rotations with handover time, active hours, ordered members,
overrides (including an assumed gap), "cover me" requests accepted in one
click, week and month views, an iCal feed, and shift reminders.
- **Notifications.** Personal rules per urgency over email, SMS, voice and web
push, verified contact methods, a real test, and an outbox with honest
statuses. Channels without a provider on the instance are shown as
unavailable, never faked.
- **Alerts in the product.** The alerts list and detail with the live
escalation card, acknowledge / snooze / resolve, incidents created from an
alert or escalated by hand with a preview of who gets paged.
- **Slack app.** Installed from Settings → Integrations in three steps
(authorize, configure, test a real message); one channel per incident with a
pinned header kept current, `/incident declare | update | escalate | lead |
status`, a :pushpin: reaction that pins a message to the timeline, a
:white_check_mark: that turns it into a follow-up, living announcements in a
channel, direct-message paging with an Acknowledge button. Every gesture goes
through the same write paths as the web. Tokens are encrypted at rest.
- **War rooms.** A Google Meet or Zoom link template attached to every new
incident, shown on the incident and in its channel.
- **Status pages.** A public page per workspace with components bound to the
catalog, 30-day bars and 90-day uptime from the impact history, public
incidents with their update timeline, scheduled maintenances with automatic
transitions, email subscribers with double opt-in and one-click unsubscribe,
RSS and Atom feeds, message templates, brand, language and a custom domain
verified by DNS. Published from the incident's update dialog when the
severity meets the page's threshold — never behind the agent's back.
- **`apps/status`.** The public pages are served by a separate, minimal app
that reads one projection and nothing else of the product: no Redis, no
session. If the product goes down, the last snapshot keeps being served; an
unknown host is a 404, never a page.
- **Reports.** Four tabs — incidents, alerts, on-call load, follow-ups — over
30, 90 or 365 days, each compared with the previous period: counts, median
time to acknowledge and to resolve, incidents per week or month, by severity
and by service, alert volume by source, recurring alerts with a shortcut to
their route, pages by hour in the recipient's local time with a night-load
warning, follow-up closure by team against the policy and what is overdue
now. Every number comes from the workspace's own rows, test incidents are
excluded, and the rows behind each tab export as CSV.
- **The assistant, governed.** One provider abstraction over any
OpenAI-compatible endpoint (`AI_API_BASE`, `AI_MODEL`); without it every
assistant function is shown as unavailable, nothing is simulated. Emails,
phone numbers, IPs, internal hostnames and secrets are redacted before a
prompt leaves. Settings → AI governance switches the whole thing or each
capability, states the data boundaries, opts private incidents in or out
of the knowledge layer and shows the log of every call — who, on what,
which model, how many tokens, how long.
- **What the assistant does — and only proposes.** A title and summary at
declaration, a summary of the timeline in the side panel, similar incidents
(by meaning when embeddings are configured, by title otherwise, and it says
which), a draft of the next status update in the update dialog, follow-up
suggestions that become real only on click, and a post-mortem drafted from
the timeline whose sections are edited in place, regenerated one by one and
always labelled as a draft. The post-mortem now moves through in progress,
in review and completed by hand.
- **Change events.** `POST /api/v1/change-events` records deploys, feature
flags and configuration changes from CI or by hand, bound to a catalog
service; the incident's side panel lists the changes recorded in the day
before it and until its resolution, and the assistant reads them.
- **Workspace logo.** Uploaded in Settings → General (SVG or PNG, one file for
light and one for dark backgrounds), stored in an S3-compatible bucket under
the workspace's own prefix, served by the product with a sandboxing policy so
an uploaded SVG can never run script, shown on the shell and on the public
status page. Without `S3_*` on the instance the row says the upload is
unavailable; a partial `S3_*` set stops the process at startup. A MinIO
service ships in the compose files for local and small deployments.
- **Dark theme, per member.** Appearance in My account: follow the device,
light or dark. Every design token has a dark value, so nothing is left
unstyled; the choice is stamped on the document before the first paint.
- **Verified workspace purge.** `pnpm workspace:purge -- --slug <slug> --yes`
erases a workspace: every `app` table, the directory rows, the sign-in
accounts no other workspace still lists, and the storage prefix — then
counts what remains in each table and lists the remaining objects, and
fails if anything is left. A test provisions a throwaway workspace, purges
it and checks the report.
- **The smoke suite works on a throwaway workspace.** Every run seeds a fresh
`smoke-<id>` workspace with the demo data, tests against it and purges it
afterwards with the operator's own command — so the demo workspace stays
clean and the purge is proven on a full workspace at every run.
- **Issue trackers: GitHub Issues, Jira, Linear.** Connected per workspace in
Settings → Integrations with credentials that are tested before being saved
and encrypted at rest. A follow-up is exported as an issue from its row and
keeps the link; every five minutes (or on demand) the issue's state comes
back, and a closed issue marks the follow-up done with a line in the
incident's timeline. Base URLs can be overridden for tests and proxies.
- **Microsoft Teams.** One Azure bot per instance; each workspace pairs its
own team by typing a six-character code the settings issued — no OAuth
dance, no shared tenant. Then the same product words as Slack: a channel per
incident (a standard channel of the team, through Microsoft Graph) with a
living header card, update and note cards, a living announcement card, a
personal card that pages someone with an Acknowledge button, the declare /
update / escalate forms as Adaptive Cards, `lead`, `status` and `help`
commands. Inbound activities are verified against the Bot Framework's
published keys; outbound calls use client credentials. Both chat tools can
be connected at once — the product fans out to every one it finds.
- **Heartbeats.** A URL per cron or job; silence beyond the interval plus the
grace raises an alert through the workspace's own managed "Heartbeats"
source — posted to the public ingest endpoint like any monitoring tool, so
routes, priorities, grouping and escalation apply unchanged — and the next
ping resolves it. Nothing is alerted before the first ping; pausing a
heartbeat forgets its last ping so resuming waits for a fresh one. Tokens
rotate; the worker sweeps every thirty seconds.
- **On-call coverage.** Every schedule shows how much of its next sixty days
has someone on call, measured against the hours its rotations declare, and
lists the gaps — an empty turn, a turn with nobody, an override that removes
the only person. Managers get one digest a day at most for the gaps of the
coming week. The reports' on-call tab carries the figure across schedules.
- **On-call pay reports.** The workspace sets its rules — hourly rates for
standby, night, weekend and public holiday, the night window, the holiday
dates — and computes a month into a draft: every quarter hour someone is on
call for a published schedule, in the schedule's zone, priced by category.
The draft is recomputed at will, then published and frozen with the rules it
was computed with; a member sees their own lines of published months; the
month exports as CSV. Availability pay, the obligation that matters in most
EU countries.
- **GitLab Issues** joins GitHub Issues, Jira and Linear as a tracker for
follow-ups: same export, same status sync back.
- **Internal status pages.** A page can be internal: to anyone outside it does
not exist — 404 on the page and its feeds, no subscription form — while a
signed-in member opens it from the product and gets a day's access through
a signed token the status app verifies without a session or a database.
- **Post-mortems exported to Confluence and Notion.** Connected per workspace
with credentials tested before being saved and encrypted at rest; from the
post-incident tab a post-mortem's written sections become a page (Confluence
storage format or Notion blocks) with a link back, and the page's address
stays on the post-mortem and in the timeline. The post-mortem here remains
the source.
- **Runbooks for the assistant.** A service carries runbooks: a GitHub or
GitLab file fetched through the forge's API (with the workspace's tracker
token when one is connected), any text address, or pasted text — refreshed
every six hours, shown on the incident's side panel, and quoted to the
assistant only when "Runbooks and documents" is switched on in AI
governance, where it is now a real source rather than a promise.
- **A catalog you shape.** Custom types with free naming and their own
attributes — text, link, choice, reference to another type, members —
created and edited from the catalog screen; entries created, edited and
deleted with a guard that refuses the deletion of anything still
referenced (incidents, follow-ups, status components, heartbeats, runbooks,
change events, other entries) and names the usages instead; a CSV import
per type that writes nothing while a single row is wrong; the write API
(`POST /catalog/types`, `POST /catalog/entries`, `DELETE /catalog/entries/{id}`,
`POST /catalog/import`) with the same validation; and a `catalog-importer`
CLI that reads Backstage (groups → teams, components → services), a
`catalog-info.yaml` on GitHub, a local YAML, JSON or CSV file, a command's
output or an inline document, then talks to the API — with `--lock` to make
the declared types read-only on screen, since code owns them. Every change
is an audit line, the API's included.
- **Enterprise edition, first features (`ee/web`, commercially licensed).**
Served by the app through one-line shells, switched on by entitlements
(`OI_ENTITLEMENTS` on a standalone install; the control plane in cloud) and
shown as unavailable otherwise:
- **Single sign-on** — OpenID Connect and SAML 2.0 connections per
workspace, configured from the settings with the redirect URI, ACS URL
and service-provider metadata the provider needs; a sign-in button per
connection; members created on first sign-in with the connection's role,
from the allowed email domains; "SSO only" refusing a password for those
domains, with a guard against locking every owner out; every sign-in an
audit line.
- **SCIM 2.0 provisioning** — one endpoint per workspace at `/scim/v2`
behind a bearer token issued once and rotated from the settings: Users
(created, renamed, deactivated, filtered by userName or externalId; PATCH
in the Okta and Entra ID shapes; DELETE deactivates, never erases) and
Groups mapped to catalog teams and their members.
- **Custom roles** — named permission sets on a built-in base. The product
now asks one question everywhere — may this member do this here? — with
twelve permissions covering incidents, catalog, on-call, status pages,
reports, each settings area and the audit log; the four built-in roles
are fixed sets, so nothing changes for them. Assigned from Members &
roles; a role in use cannot be deleted.
- **The user guide, inside the product.** Twenty-seven Markdown chapters under
`docs/guide` — install and configure, concepts, every screen, the
integrations, the enterprise edition, operations, eight end-to-end use
cases (including the complete configuration for Open Helpdesk, our own
support desk, with an honest assessment of what works as-is), a glossary —
rendered under **Guide** in the rail for every member,
with a table of contents per chapter and real screenshots captured from the
demo workspace by `pnpm --filter @openincident/smoke guide-shots`. The files
carry no product markup, so the public website can serve them as they are.
The members list now shows where a member came from (SSO, SCIM).
- **QA from the admin.** Settings → QA (owners) runs the repository's own
test suites from the product — the Playwright smoke suite on a throwaway
workspace, the unit tests, the type check, the linter, the formatter —
through the worker on the machine that has the source checkout. Each run
is a row with its live log, exit code, counts and the list of what failed;
a run can be stopped; prerequisites are checked on screen and an instance
without a checkout says so instead of pretending.
- **Auth base URL derived per request.** Better Auth now resolves its base
URL from the workspace host (checked against the instance's domains) unless
`BETTER_AUTH_URL` pins it, so OAuth and SSO callbacks land on the workspace
that started the flow.
### Fixed
- **The demo's public status page pointed at a domain nobody serves.** The
demo seed declared `status.skylark.dev` as a _verified_ custom domain, so
"View the public page", the incident's status page link and the API answered
`https://status.skylark.dev` instead of the instance's own address. The
demo's custom domain now stays pending, as a sample; a domain becomes the
page's address only once its DNS is really verified.
- **The compose stack now names its own public addresses.** `BASE_DOMAIN` and
`STATUS_BASE_DOMAIN` default to the compose ports (`localhost:3000`,
`status.localhost:3001`) unless `.env` names real domains; previously the
development values of `.env.example` (`localhost:3100`, `:3107`) leaked into
the production stack and every link — and the workspace resolution — pointed
at ports nothing listened on.
- **Workspace resolution after a server-action redirect.** Next.js renders the
target of a redirect issued by a server action through an internal request
whose `Host` is the listen address, with the real host in
`x-forwarded-host`. The middleware read `Host` alone, so on an instance with
`DEFAULT_TENANT_SLUG` set the page that followed a save could be rendered
for the default workspace instead of the caller's. Found by running the
smoke suite on a throwaway workspace next to the demo one; the middleware now
prefers `x-forwarded-host`, which is also what any reverse proxy sends.
- **Origins built after a server-action redirect.** The same internal request
made URLs rebuilt from `Host` point at the bare listen address (a heartbeat
ping URL without its workspace, for instance). Origins now honour
`x-forwarded-host` and `x-forwarded-proto` first.