Architecture Decision Records
Binding. Design, code, dependencies and APIs comply with every accepted ADR here. Before proposing or implementing, skim this directory for decisions touching your scope. A new significant decision — adding a technology, changing a pattern, departing from a convention — needs a new sequential ADR before or with the change. Never edit an accepted ADR's decision; supersede it.
Format: NNNN-kebab-title.md, English, Nygard style (000-template.md).
| ADR | Decision |
|---|---|
| 0001 | Name "Artemis Studio"; trademark risk accepted with a cheap rename path (superseded by 0201) |
| 0002 | Jolokia-first transport, Core client second, capability-gated features |
| 0003 | Real-time updates over SSE |
| 0004 | Topology by seed node + auto-discovery |
| 0005 | React 19 + Vite + Mantine 9 |
| 0006 | Own the metrics timeseries in PostgreSQL |
| 0007 | One container image, Docker Compose first |
| 0008 | Liquibase migrations; schema physical tuning |
| 0009 | |
| 0010 | Jolokia over a blocking RestClient; spring-boot-starter-webflux removed |
| 0011 | Persistence via JPA (Hibernate) mapped to the Liquibase-owned schema |
| 0012 | Split-brain detection requires corroborated evidence (amends 0002) |
| 0013 | A cluster is registered from a list of seed URLs (amends 0004) |
| 0014 | Lombok for boilerplate, MapStruct for layer-to-layer mapping |
| 0015 | Tiered scrape scheduler; refresh-cycle counter scheduler-owned per cluster (retires HaRefreshTask) |
| 0016 | queue_snapshot written by JDBC batch upsert, not JPA (scoped exception to 0011) |
| 0017 | Cross-node aggregation — one logical node per NodeID, scrape the live endpoint only |
| 0018 | SSE hub is SseEmitter on Spring MVC, carrying poll-derived change signals (annotates 0003; extended by 0027) |
| 0019 | Frontend API types generated from the backend's OpenAPI document |
| 0020 | The virtualized data grid lays out on shared CSS grid tracks, not <table> |
| 0021 | |
| 0022 | Dry-run is a broker-side estimate; the bulk safety cap is server-enforced |
| 0023 | |
| 0024 | Frontend DOM test harness is Vitest + Testing Library + MSW |
| 0025 | Scrape cadence applies without a restart, via SchedulingConfigurer |
| 0026 | Core client — one subscription per live node, poll loop, Studio-driven reconnect, cached capability verdict (extended by 0031) |
| 0027 | The SSE events topic carries data, with Last-Event-ID replay and coalesced derived signals (extends 0018) |
| 0028 | broker_event — buffered batch insert, bounded queue with a visible drop counter, seq PK |
| 0029 | MessageTransport — Core for read/write fidelity, Jolokia for mutations and deep pages (supersedes 0021) |
| 0030 | Request-reply correlation is notification-anchored and browse-sampled, with a disclosed coverage ceiling |
| 0031 | Pooled Core connections via pooled-jms, superseding connect-per-call |
| 0032 | Request-reply latency via Micrometer time-windowed percentiles, no persisted history in Phase 5 |
| 0033 | Metric read model — date_bin bucketing, gauge vs. counter-rate, server-clamped step/range |
| 0034 | Two-section collapsible sidebar (cluster switcher + per-cluster view nav), replacing the horizontal view strip |
| 0035 | Alert rules are a discriminated union (metric threshold / cluster state); evaluation rides the scrape tiers, not an independent timer |
| 0036 | Notification delivery is a durable Postgres queue, batched per rule per tick, with Standard Webhooks signing |
| 0037 | Session-cookie authentication (not bearer tokens) for the browser, since EventSource cannot set headers |
| 0038 | Fully dynamic permission strings, resolved once per request via a cluster→environment→global scope walk (amended by 0172: scopes add resource patterns and teams) |
| 0039 | API tokens — SHA-256 hash, prefix lookup, grants intersected with the live owner |
| 0040 | OIDC — JIT provisioning, principal swap after the exchange, claim mapping re-applied every login |
| 0041 | Audit actor carries real identity and token attribution (supersedes 0023) |
| 0042 | CalVer releases published to Docker Hub on every push to main (complements 0007) |
| 0043 | Broker configuration is compared by a classified pointer diff, not a diff library (amended by 0178: every node is compared, not a pair) |
| 0044 | Slow-consumer detection has two authorities, and the broker wins |
| 0045 | The MCP server is a capability surface (~13 intent-shaped tools), not a REST mirror; the token budget is a build failure |
| 0046 | MCP authenticates with the ADR-0039 personal API tokens — one credential store, one authorization model |
| 0047 | Two configuration planes — studio_setting (operator, live, audited) and a Spring Cloud bootstrap plane (deploy-time, {cipher}); no config server |
| 0048 | Every settings-tunable schedule is a re-reading trigger task, not a @Scheduled annotation (extends 0025) |
| 0049 | Queue and address lifecycle is a cluster-wide fan-out that reports per node, never rolls back, and makes managementWrite evidence-backed |
| 0050 | (superseded by 0054) The MCP listing budget scales per tool and enum/body detail moves to studio://tools (extends 0045) |
| 0051 | The changelog is generated from commit messages, one file per release (supersedes 0042's changelog mechanics) |
| 0052 | One global freshness indicator scoped to observed queries; the stream reconnects forever with a silence watchdog (modifies 0018) |
| 0053 | Broker time is normalised onto Studio's clock; skew is measured from the Jolokia response timestamp, disclosed per flow, and alerted (extends 0030, 0035) |
| 0054 | MCP discovery is the studio_help tool, not an optional resource; one catalogue generates the schemas, help, resource and server instructions (supersedes 0050) |
| 0055 | Metric charts carry time on the x-axis, not array position; a relative window advances quantized to the bucket (extends 0033) |
| 0056 | Every view is bounded and says so; the topology canvas degrades by level of detail above a node threshold (extends 0020) |
| 0057 | Connection identifiers are node-local and ephemeral: a close by id names a node, a vanished target is a success, and the confirmation names the client (extends 0049) |
| 0058 | The SQL Console is a restricted dialect parsed to an AST and validated against a whitelist; predicates split into selector pushdown and residual scan; SELECT-only (extends 0021, 0022) |
| 0059 | |
| 0060 | |
| 0061 | The documentation site is VitePress under site/, deployed to GitHub Pages by its own Actions workflow; docs/ stays the single source and the README is trilingual |
| 0062 | Message capture is a non-exclusive divert into a ring-bounded Studio queue drained by a Core consumer; address-scoped, per-node, instance-owned (supersedes 0059, 0060) |
| 0063 | Full-text over the message index is Postgres tsvector with the simple configuration and websearch_to_tsquery; no second datastore (extends 0059, 0062) |
| 0064 | A console query is submitted by POST and streamed by reference, so query text never reaches a URL (modifies 0058, departs from 0003 for this stream) |
| 0065 | Measured on Artemis 2.56.0: diverts, address settings and security settings created over management survive a broker restart, so the hazard is configuration drift rather than evaporation and a divert's origin is not derivable (corrects 0062, corrects the assumption absorbed from change 04) |
| 0066 | The capture address auto-creates and a tap is healthy only while it is being drained: measured on Artemis 2.56.0, a divert replicates to a promoted backup but its non-durable queue does not, and the producer — not Studio — is what fails (refines 0062) |
| 0067 | A cluster's address settings, security settings, diverts, addresses and queues are declared in Studio, applied over the management API canary-first and halting on the first failure, with hazards named before any write, replace semantics disclosed, only Studio-applied items ever removed, and drift advisory — evaluation scheduled, action never (extends 0049, 0065; records the advisory-only decision change 05 intended; absorbs change 05) |
| 0068 | The capability probe's appliable gaps become a declaration the operator saves in one action — seeded from the node so replace semantics hold no surprises, recorded as RECOMMENDED, applied through the ordinary canary-first gates; the gaps Studio can never close are named with their fragment, and a capability is read back rather than asserted (extends 0067, 0049, 0065) |
| 0069 | Studio is a kernel + platform + feature-plugin modular monolith, feature-first on both sides, composed at build time, gated by artemis-studio.features.<id>.enabled, and verified by Spring Modulith and ArchUnit in one Maven module |
| 0070 | A versioned extension contract (backend descriptor + SPI beans, frontend defineFeature + slots), a server-published feature manifest, and a closed set of navigation groups (amends 0034) |
| 0071 | Every broker write runs through BrokerCommands: guard, audit before the call, dry run, cap, per-node fan-out, audit outcome (formalises 0022, 0049) |
| 0072 | Each module owns its tables; the schema history is re-baselined once per module, breaking upgrades while pre-stable; audit events drop their foreign keys (supersedes 0008's history continuity) |
| 0073 | Identity is a sealed provider SPI (credential, redirect, bearer); external users are keyed by provider and subject and group mappings are per provider (supersedes 0040's mapping model) |
| 0074 | Frontend kernel / ui / feature / app boundaries are enforced with eslint-plugin-boundaries |
| 0075 | Message content passes a content policy at typed choke points: rules and checksum detectors mask on every egress path, credentials are dropped, stored copies are masked with sealed originals, message:clear shows the rest by grant and is audited (constrains the parked replay change) |
| 0076 | The per-node rate-limit permit is taken inside JolokiaBrokerClient before every request, keyed by the Jolokia URL; a batch costs one permit per 50 operations |
| 0077 | Capture acknowledges only what it stored: each drain writes its own batch, acknowledges after commit, and backs off with jitter inside its bound on a failed write |
| 0078 | The pending audit row commits in its own transaction before the broker call and the outcome in a second one after it; a rolled-back caller marks the row failed |
| 0079 | Capture broker objects are scoped per Studio instance, the capture queue is bounded in bytes, and the capture prefix is reserved |
| 0080 | The flow graph is laid out by ELK layered in a web worker with fixed columns, and shows flow as budgeted SVG animateMotion dots that pause natively and never carry meaning alone (rate encoding superseded by 0095) |
| 0081 | Client activity is sampled only while a cluster's flow is observed: a lease renewed by flow reads, the cluster lock, one POST per node carrying client listings and routing reads, per-object deltas into shared disposable caches |
| 0082 | Configuration apply updates a divergent queue (read-merge updateQueue) or address (updateAddress, dropping a bound routing type is High) instead of reporting a finding it cannot close; it still never destroys a queue or an address (extends 0067 D6) |
| 0083 | Apply keeps a routing type that queues of that type are bound to and reports it as a DIVERGENT_ADDRESS finding, because the broker refuses the removal (amends 0082 D2) |
| 0084 | Queue delete checks each node first: it removes the diverts that forward into the queue's address when it is the address's last queue (named in the preview, deleted before the queue, with a warning that a declared one comes back), keeps diverts whose source is that address and capture taps, and disconnects consumers only when disconnectConsumers is set (extends 0049, 0071) |
| 0085 | Queue delete removes a divert into the queue's address only when the delete leaves that address with no queue and no divert bound; a divert from the address keeps it bound, so the incoming divert is kept and named (amends 0084 D1) |
| 0086 | A SQL query with no source qualifier reads the index only where an enabled CAPTURE subscription covers every target; a queue that is only sampled is read from the live brokers, and FROM index."Q" still reads a sampled index (extends 0058 D3) |
| 0087 | The Configuration screen is one desired-vs-live view with a per-row live state in words and a status bar; plan, hazards, confirmation and result move into a drawer on it, the separate apply route and the drift tab are deleted, and a row's "Apply this" narrows the plan by step identifier with the hash recomputed over the narrowed plan (extends 0067 D3/D7/D12) |
| 0088 | CI jobs run only for the paths a change touches; a push to main releases only when it changes what the image is built from (supersedes 0042's every-push trigger) |
| 0089 | Consumer health is one fixed, ordered verdict ladder computed once in feature/triage and shared by the screen, the REST API, the MCP tool and an alert condition; deliveringCount separates a stalled consumer from a starved one, the broker's own verdict still wins, and an uncomputable verdict is never reported as healthy (extends 0044; makes AlertCondition a real extension point) |
| 0090 | The routing builder edits the declaration and is a mode of the one configuration screen: composing a route writes a document, the apply drawer stays the preview, the graph is keyboard-operable, bounded, motionless and never the only path, and element state is declared-versus-observed rather than origin (extends 0067, 0087; applies 0065 D2, 0080) |
| 0091 | Bridges become declared items applied by the configuration engine, superseding the prohibition on mutating them: measured on 2.44.0 — a hyphenated createBridge(String) document, a nested transformer-configuration, unknown keys accepted and ignored, thirteen of twenty-three fields read back, concurrency suffixing the object name, and everything surviving a restart; a change is a remove-and-create, verification claims only what the broker reports, and a transformer is ordinary drift (supersedes routing-management's bridge prohibition; extends 0049, 0065, 0067, 0071) |
| 0092 | A bridge's credential lives in SecretVault and the declaration carries only a reference; six disclosure paths — document, revision, diff, audit parameters, MCP responses, export — are closed explicitly with a test each, export names the credential it omits, and a wrong password surfaces as started-but-not-connected rather than as drift (extends 0009, 0075; depends on 0091 D5) |
| 0093 | A bulk run is one operation over a preview-frozen, hashed queue set, executed sequentially through the unchanged single-queue commands on a persisted, stoppable run; restart interrupts and never resumes; queue-count cap without override plus the message cap; audit children linked by parent_id bound through a ScopedValue (builds on 0022, 0069, 0070, 0078) |
| 0094 | The routing builder is the Routing screen's Builder tab, contributed by brokerconfig through a new routing.tabs kernel slot, with its own apply drawer; Configuration's routing tab is removed without a redirect; a divert's target queue is declared inline from the divert editor as its own revision, never created on a broker (supersedes 0090 D3's host screen; keeps 0090 D1/D2) |
| 0095 | Flow edges take width (2–10px), dot speed (6–1.8 s) and dot count from one square-root value of the rate capped at 1000 msg/s; weight is monochrome, the line state sets only the dash, and the legend draws real reference lines (supersedes 0080's encoding clause) |
| 0096 | A primitive property is required in the OpenAPI contract, requests and responses alike, set once by a springdoc PropertyCustomizer, because Jackson 3 refuses a body that omits one; a field that may be absent is a nullable reference type, and each endpoint is tested over HTTP with the payloads the frontend sends |
| 0097 | A cross-broker move stages the frozen selection in a Studio-owned durable queue on the source broker, relays it over the Core API committing target before source with _AMQ_DUPL_ID, and is resumable or returnable; copy browses with a Postgres id ledger; targets that would silently drop are refused (builds on 0022, 0078, 0093) |
| 0098 | Core TLS trust is per cluster through a Studio SSLContextFactory keyed by the sslContext transport parameter, never the JVM default context |
| 0099 | Runtime plugins are uploaded in the UI into Postgres and run as Spring child contexts (own classloader, curated @PluginApi parent, host-imported infrastructure) behind one HTTP gateway and dynamic registries; activation is Instant, Brief maintenance or Restart; a plugin never stops Studio (safe mode) (supersedes in part 0069) |
| 0100 | Plugin UIs are Module Federation remotes that take React, Mantine, TanStack and the host's SDK as import:false singletons; bootstrap loads remotes before creating the router; plugin routes live under /p/<id>/ |
| 0101 | Each plugin owns schema plugin_<id>, a 3-connection pool and its own EntityManager factory; migrations via Liquibase CommandScope under an advisory lock; no objects or foreign keys in public (amends 0072) |
| 0102 | The plain jar is the plugin API on Maven Central (runnable jar becomes -exec), @PluginApi types are japicmp-gated against Contract.VERSION, and @artemis-studio/plugin-sdk is on npm via trusted publishing (amends 0042) |
| 0103 | Plugin management belongs to an installer tier kept outside roles and checked per request, and every lifecycle action needs a ≤5-minute step-up (password, or OIDC prompt=login/max_age bound to the same subject); session ids rotate on sign-in |
| 0104 | A plugin that needs a restart has Studio exit gracefully (code 75) for its supervisor to start it again — only when plugins.restart.supervised is set (compose) or on Kubernetes, with the installer's consent in the review, or by a guarded, audited, cooled-down "Restart Studio"; never the actuator restart endpoint (amends 0099) |
| 0105 | Email (Jakarta Mail), Microsoft Teams (Adaptive Card) and PagerDuty Events v2 channels on the existing delivery path; PagerDuty deduplicates per rule and subject so a resent row is safe |
| 0106 | Setup review is a pure rule catalogue over one batched read per node, persisted per cluster, with audited risk acceptance and a SETUP_RISK alert signal |
| 0107 | Row actions are per-resource kernel slots (queue.actions …) with a closed set of sections, rendered in one anchored menu per grid (right-click, Actions button, Shift+F10); blocked items stay focusable with their reason; dialogs live in a host that outlives the row; link slots are built-in only; Flow menus navigate only (amends 0070) |
| 0108 | VirtualTable is one tab stop with APG roving cell focus: Enter opens the row, Space selects, Shift+F10 opens its menu; active cell tracked by key and always rendered; widget cells focus their control; full values copyable by keyboard (extends 0020) |
| 0109 | Cluster switch keeps the view; title and breadcrumb from a kernel store; palette gets the query and searches only snapshots while open; recents; declared g-letter, ? and / shortcuts that are ignored in editors, dialogs and menus and can be turned off (extends 0034, 0052) |
| 0110 | splitBy=NODE per-queue metric series and opt-in byNode flow breakdown from the per-node rows; Flow Split layout with a metrics-contributed trend slot and small multiples; cluster gauge averaging recorded as a defect (amends 0033, extends 0095) |
| 0111 | Plugins get beans scoped to them (PluginSecrets, PluginMessaging), and message through registrations Studio converges like capture |
| 0112 | A registration states its concurrency (consumer 1..32, tap 1); consumers take no prefetch (consumerWindowSize=0); group order is the broker's message grouping; plugin drains run on Core factories with their own bounded thread pools (max-threads); contract version 2 (amends 0111) |
| 0113 | Plugins declare metrics and implement PluginMetricSource; Studio samples them on tier B into metric_sample (PLUGIN), a Prometheus MultiGauge and threshold rules; declared default rules are seeded once per cluster; SDK MetricChart |
| 0114 | Plugin MCP tools declare permission, scope and params in plugin.json; McpPluginBridge checks the permission before the tool runs (cluster denials hidden, global ones named) and refuses undeclared tools, a cluster tool without clusterId and a posture that contradicts readOnlyHint; contract 3 |
| 0115 | SDK CodeEditor on CodeMirror with @codemirror/lang-yaml and lang-json; diagnostics by line and column come from the caller; theme shared with the SQL editor |
| 0116 | VirtualTable columns fit their content (180–480 px) and resize by drag, double-click or Ctrl+Shift+Arrow; widths per viewer under a storageKey |
| 0117 | The SDK's DiagramView: a read-only, keyboard-operable diagram on xyflow and the ELK worker, fed plain nodes and edges |
| 0118 | Pause is enforced by TanStack's focusManager for every query, plugins included; poll() and the refresh control are removed (amends 0052) |
| 0119 | Topology rediscovery is a scrape tier on scrape.discovery-interval (1m); the on-demand endpoint and its buttons are removed (implements 0004) |
| 0120 | DiagramView offers caller-defined inserts on arrows (insertable, insertChoices, onInsert), box actions (nodeActions, onNodeAction; "⋯", right-click, Shift+F10), zoom controls and a CSS height; it still changes nothing itself (amends 0117) |
| 0121 | The English site home is one hand-written file in site/src/public/ (inline CSS/JS, GSAP from cdnjs), an interactive story of the one-broker-per-tab hunt; the logo links to it with a full load (amends 0061) |
| 0122 | Superseded by 0168. Every response carries a Content-Security-Policy (scripts, connections and workers from self, frame-ancestors 'none'); every plugin SVG is sandboxed |
| 0123 | Sessions really live in JDBC (spring-boot-starter-session-jdbc); disabling a user, removing a grant or changing a role ends the affected users' sessions (amends 0037, 0103) |
| 0124 | OSV-Scanner (every manifest; PRs fail on new, main and weekly on any; amended by 0171: only fixable ones fail) and CodeQL (code-scanning default setup) run on every PR; findings are fixed, false positives dismissed with a reason |
| 0125 | Every ScheduledJob declares INSTANCE or INSTALLATION; installation-wide jobs run on one instance per tick through ShedLock (keep-alive, database time); plugin contract 4 |
| 0126 | Pull requests carry the verification (parallel, path-filtered jobs behind one required ci-ok check on an up-to-date branch); main does not re-test, and releases publish to Central and npm only when their inputs changed since the newest version there (supersedes parts of 0088, 0124) |
| 0127 | A cluster-scope gauge is the sum of every queue's last sample at or before each bucket's end, within the queue's own sampling interval, with no bucket peak (supersedes 0110 D4) |
| 0128 | Backend (JaCoCo, merged across CI shards) and UI (Vitest V8) coverage is measured on every PR that runs their tests and reported in the job summary and as HTML artifacts; no threshold gates a build yet |
| 0129 | A release tags the merge commit and pushes only the tag; its notes are the GitHub release body, the site fetches them for releases after 2026.09.60, and the release deploy key is removed (supersedes 0051's per-release file, 0126 D5) |
| 0130 | One permission catalogue (FeatureRegistry.catalogue(), built-ins then active plugins) for every reader; a permission declares globalOnly; a runtime permissions health check compares guards and plugin manifests with the catalogue and reports, never blocks (amended by 0172: the scope is enforced, globalOnly replaced by scope) |
| 0131 | Everything under web/ is formatted by Prettier 3 (120 columns, single quotes, trailing commas), checked in the frontend CI job and applied by just fmt; the one-time reformat is in .git-blame-ignore-revs |
| 0132 | Every stored secret is sealed under its own data key, wrapped by a versioned key-encryption key from one provider (env, file, Vault, Kubernetes); the current version is stored and changed by an online, resumable, audited rotation (supersedes 0009's key handling) |
| 0133 | One SecretRedactor masks credential-like values in logs, audit parameters, error details and the broker.xml export; a leak test with planted secrets is the proof (extends 0092) |
| 0134 | Every store that grows with use is a ManagedStore (@PluginApi HousekeepingContributor) with bounded retention settings, purged in batches by one installation-wide housekeeping job, one PURGE_STORE audit event per store; a build test fails for a table with no store |
| 0135 | An alert rule without a cluster is installation-scoped, evaluated by InstallationSignalSources; STORAGE_QUOTA and STORAGE_HEALTH are seeded once per installation |
| 0136 | API tokens rotate with a 24-hour overlap (previous secret in three columns, a late use audited), live under a live lifetime cap (90 days by default; expires_at required), are rate and concurrency limited per token and per user (fixed windows, RateLimit-*, 429), are counted per hour in api_token_usage, and are listed and revoked by token:admin holders (amends 0039) |
| 0137 | McpGate wraps the SDK handler at the stateless transport: per-caller instructions and tool list, a token's tool allow-list answered as unknown tools, an installation-wide read-only mode, and an MCP_TOOL_CALL audit row parenting every call; no permission check is copied (amends 0046, 0054) |
| 0138 | SettingDef.Kind.BOOLEAN, rendered as a switch |
| 0139 | Every release artifact carries keyless GitHub attestations (SLSA provenance, CycloneDX SBOMs from Syft), image ones pushed to Docker Hub as referrers; pull requests gate the image on fixable high or critical Grype findings (amends 0042, 0102) |
| 0140 | CI analyses every PR and main push with SonarQube Cloud (Maven scanner, JaCoCo + Vitest lcov coverage); a failing quality gate fails ci-ok; each change resolves its issues (fix, or false positive with a reason, or a scoped exclusion recorded in the ADR) |
| 0141 | Plugin jars are standard signed JARs verified offline (one signer, every entry covered) against administrator-pinned publisher keys identified by public-key fingerprint; the trust decision is computed at read time, activation enforces it with acknowledgement, an off-by-default audited allowance admits unverified plugins, removing a key flags its plugins and degrades health (amends 0099, 0103) |
| 0142 | Artemis 2.33.0 to 2.57.0 is supported (BrokerVersion): registration refuses an older seed, CI tests both ends, the JSON createDivert falls back to its positional arm before 2.38, and an operation that cannot fall back is a per-node VersionGate |
| 0143 | Local accounts enrol TOTP (RFC 6238 on the JDK) and WebAuthn passkeys (Spring's WebAuthnRelyingPartyOperations) with recovery codes; roles carry requires_mfa (ADMIN by default) for local sessions only; admin-set trusted devices; tokens record MFA at mint; break-glass recovery at startup |
| 0144 | Password policy (length, 72-byte cap, not the username, offline common-password list, optional HIBP k-anonymity); trusted proxies only; per-account DB lockout in its own transaction plus per-source and per-IP throttles; locked answers like wrong |
| 0145 | Idle (user activity via X-Studio-Activity) and absolute session lifetimes as runtime settings; users and admins list and end sessions by hashed handle |
| 0146 | Support bundle built in-process: redacted in-memory log buffer, a previewed snapshot the zip download is written from, diagnostics:bundle, audited |
| 0147 | OTLP export of metrics, traces and logs through Boot's OpenTelemetry starter, off by default behind one switch; Micrometer Observation is the only instrumentation API (HTTP, Jolokia, Core, jobs, JDBC); SecretRedactor runs again at export on spans and OTel logs; nodes are labelled host:port, never by URL |
| 0148 | Text carried in a bytes message decoded once where Studio reads it (strict UTF-8, gzip/deflate up to 2 MiB); binary is a property of the body, not the message type |
| 0149 | The public API is one contract: a break passes CI only with a ! / BREAKING CHANGE: commit (oasdiff against main, which every release publishes), one {data, page, pageSize, count, hasNext} list shape, problem+json for every error, Spring API versioning with built-in deprecation headers, stated limits |
| 0150 | Idempotency-Key on every mutating /api/v1 request: persisted, scoped to the user, fingerprinted, replayed for 24 hours (a ManagedStore) |
| 0151 | Every release attaches the attested OpenAPI document and publishes generated @artemis-studio/client (npm) and artemis-studio-client (Central) (revisits 0019 in part) |
| 0152 | Replicas coordinate through Postgres: a replica registry with heartbeats, a LISTEN/NOTIFY bus for stream frames and cache signals, per-cluster ownership leases placed by rendezvous hashing, a drain phase and /livez /readyz probes |
| 0153 | A plugin declares that it needs a license, Studio stores the file as opaque bytes and shows the verdict the plugin reports, and the plugin is told when the file changes (PluginLicense, PluginLicenseChanged, StudioInfo.brokerInstances()) |
| 0154 | Plugins read metric history through the @PluginApi MetricHistory as a named acting user, whose account and grants are re-read on every read, through the metrics read API's own checks and clamps |
| 0155 | CI's Sonar analysis skips pull requests opened by Dependabot, whose runs get no SONAR_TOKEN; the analysis of the main push after the merge covers them (amends 0140) |
| 0156 | Plugins sign users in through declared PluginCredentialProviders that answer with an identity, never a principal, on the one login path; verified plugins only, confirmed on install; Studio revalidates subjects every 5 minutes; second factors cover every password account (amends 0143) |
| 0157 | One token scale: primitives in theme.ts with a cssVariablesResolver passed to MantineProvider, semantic --as-* in theme.css, components read only variables (no colour-name props, raw colours or px); reduced motion zeroes the duration tokens; autoContrast for filled controls |
| 0158 | Atkinson Hyperlegible Next (UI, identifiers, tabular figures) and Mono (code, bodies) as pinned @fontsource-variable packages bundled by Vite and served by Studio, never a CDN; Latin preloaded with metric-matched fallbacks; graphite, cobalt, amber and signal palette; health is quiet and stated in words |
| 0159 | The colour scheme follows the system, with system, light and dark chosen from one header control; a static same-origin boot-prefs.js applies scheme and density before first paint under the CSP (amends 0005) |
| 0160 | One DataTable over one column model: a virtualised CSS-grid renderer (keeps 0020, 0107, 0108) and a native static table for small read-only sets that switches to the grid above 200 rows; DescriptionList for term and value pairs; replaces VirtualTable and the raw tables (refines 0020, 0056) |
| 0161 | Columns are measured in a max-content Measurer with per-kind fonts and ch bounds, then placed by a pure solver: fit, shrink, hide by priority inline-end first with hysteresis, then overflow with sticky edges; viewer widths are hard; identity and node columns never hide; state {v, widths, hidden, shown, order} (supersedes 0116, keeping its resize and keyboard interactions) |
| 0162 | Compact (default) and comfortable table density, a per-browser viewer preference applied to every table before first paint, never in the URL; compact targets stay at least 24 px |
| 0163 | Pages are built from shared parts (Page, PageHeader with the single h1, Section, Toolbar, the empty, error and loading states, StatusBadge, Stat, DescriptionList, ConfirmDialog, notify), all exported by the SDK; VirtualTable removed and the extension contract bumped so old plugins are refused rather than crash; exports frozen per release |
| 0164 | Laid out for desktop windows from 1280 px at 100% zoom with no phone or tablet layouts; intrinsic layouts hold at 200% zoom, the nav collapses to its rail below 64rem; grids, canvases and the editor are the 2-D reflow exceptions |
| 0165 | Layout and accessibility are verified in Chromium: a Vitest browser-mode project with a tables test over every grid's real columns, and a Playwright sweep of every route on an isolated QA stack checking axe, overflow, CLS, CSP and foreign requests (amends 0024) |
| 0166 | Trusted publisher keys can be pinned by configuration (artemis-studio.plugins.trusted-keys, a source of ADMIN or CONFIGURATION on each key): the table is reconciled before plugins start at boot, a key dropped from configuration is un-trusted, a bad entry fails startup, a configured key cannot be removed through the API or UI, and every change is audited as configuration (amends 0141 D3, D7) |
| 0167 | A broker is registered in one cluster, identified by its NodeID (the URL in normal form when it reports none): the check and the registration refuse an overlap, partial or full, with a 409 cluster-already-registered naming the cluster when the caller may see it, and a broker_identity table keyed by identity makes the second of two racing registrations find the first; claims follow the nodes (discovery, NodeID changes, URL overrides) and a claim no node backs is released, and a cloned broker sharing a NodeID is refused with no override |
| 0168 | XSS is defended at output and in the browser, never by rewriting input: the CSP adds script-src-attr 'none' and Trusted Types (policies default for markup-free text, dompurify, studio#worker), every response carries Referrer-Policy, Permissions-Policy and Cross-Origin policies, downloads are attachments, lint bans HTML sinks, and links from data go through safeHref. Supersedes 0122 |
| 0169 | |
| 0170 | The strict up-to-date rule stays, and a pr-auto-update workflow merges main into the open PRs with auto-merge on one at a time, oldest auto-merge first, as a serial queue, with a fine-grained token so CI re-runs and auto-merge merges the tested tree (supersedes 0169) |
| 0171 | OSV findings are all reported to code scanning, but only one with a fixed version fails CI (main, weekly, and what a PR introduces); a finding with no fix is a ::warning:: (amends 0124) |
| 0172 | Teams own queue and address name patterns per cluster (no overlap; members, team roles, shares); every permission declares global, cluster or resource scope and what it requires; every check names its resource (404 when unreadable, 403 when forbidden), lists and streams are filtered, access is read per request (amends 0038 and 0130) |
| 0175 | A cluster stores a management URL pattern (scheme://{host}:port/path, by default its first seed); a discovered node's management URL is the pattern with its connector host, attached only when the broker answering reports its NodeID, with a recorded source (seed, derived, manual) and a classified reason when there is none; a seed host with several addresses fans out (a TLS seed keeps its host name); the management and Core accounts are tried separately and a refusal is CREDENTIALS_REJECTED naming the account (amends 0013, 0004) |
| 0176 | The registration check previews what adopting the running configuration would declare (counts and node disagreements), and POST /clusters with adopt saves it as revision 1 in the registration's transaction, attributed to the operator; on when the nodes agree, off when they do not; nothing adopts on its own after registration (amends 0067 D8) |
| 0177 | A recommended security setting's roles are the Core account's roles read with listUser (those the broker already names for the address, else all of them); when the broker cannot list users the broker's own roles are prefilled and the reason is stated, with the source as accountRolesSource (amends 0068) |
| 0178 | Config diff compares every manageable node at once: per key a majority (more than half of the nodes that return it), its outliers, a state (SAME, DIFFERENT, MISSING_ON_SOME) and ADR-0043's class (DRIFT, EXPECTED, UNCLASSIFIED); an unreadable node is listed with its reason and left out of every majority, and with fewer than two answering there is no comparison; the view opens on drift, with the filters in the URL (amends 0043 D1 and D5) |
| 0179 | Gated service methods call OperationGate.run after authorizing and never on a dry run (@Gated, checked by ArchUnit); the contract is the @PluginApi module kernel.gate; one plugin approval provider, armed from its desired install status with no cache, answers allow, hold or deny; armed and unavailable fails closed (503); Studio owns held operations; contract 12 |
| 0180 | A held operation's parameters are canonical JSON bound by a SHA-256 hash, its request and decision AES-GCM sealed, its states kept by triggers; a vote and a run are each one conditional update; the run replays the same service method as the live requester after integrity, version, stateKey and provider checks; BY_REQUESTER for secret results; an interrupted run is OUTCOME_UNKNOWN, never rerun |
| 0181 | Deciding is session-only with fresh authentication; for every provider the approver is another person (id, external identity, email), holds the approver permission now, has an older account, and the requester made no access change to anyone since the request (access_change_log); session facts go to the provider |
| 0182 | A per-recipient inbox_item table (in-app links only, size checks, dedupe, bounded unread count) and one stream per user (/api/v1/me/stream); the replica bus carries a data-free signal and clients refetch; plugins post through a scoped @PluginApi Inbox |
| 0183 | alert_delivery carries alert and notice kinds; plugins list channels without their configuration and enqueue rate- and size-limited notices with in-app links made absolute from the public URL; notices never carry a way to act (amends 0036) |
| 0184 | artemis-studio.gate.break-glass is accepted only from the OS environment or a JVM system property (any other source fails startup); while set, gated operations run audited GATE_BYPASSED with a banner, logs and inbox items; held requests stay held |
| 0185 | Settings changes apply as one gated change set with a stateKey over current values and a preview; Settings shows a form per contribution with search, defaults, one draft and request-approval review; Administration uses grouped vertical navigation (Access, Installation, Governance, Support) |
| 0186 | Every danger-tone confirmation is a button held until it fills (--as-hold-duration, 1.5 s) with the mouse, a finger or Space/Enter; a click or a press let go early does nothing; the button names the action and count; focus starts on Cancel; ConfirmDialog.typedName and ConfirmByTyping are removed. |
| 0187 | The gate needs two approvers while it holds anything (ApproverPool, ApprovalProvider.enforcing()); an access change that would lower them below two is refused before commit; /gate/status reports quorate; break-glass stays the recovery; the requester's reason is removed. Contract 13. |
| 0188 | NavItem/NavGroup/SectionNav are the one navigation row, heading and link list (Settings, Administration, Configuration); a section change keeps the scroll; the sidebar shows only under a cluster, other pages get a back line; pair rules re-check on every edit. |
| 0189 | Mantine's dialogs, drawers, menus, popovers, tooltips, collapses and progress move on the motion tokens from theme defaults, exits faster than entrances, menus from their trigger; keyboard-opened surfaces appear at once; press feedback is a 0.97 scale and hover is pointer-only; toasts sit under the header, three at most; Flow animates at most 150 dots (amends 0080, 0095); the colour scheme is chosen in the user menu (amends 0159) |
| 0190 | Audit rows carry a commit-order position; Studio pushes records to declared receivers and advances their cursor on return; finished rows are guarded; retention waits for acknowledgement up to a maximum age, then records a gap and alerts; declared plugins can hold records back, and a failing hold purges nothing |
| 0191 | Audit export as CSV, JSON or NDJSON behind audit:export, with the audit view's visibility and redaction, streamed from a cursor, CSV formula cells neutralised, and every export audited |
| 0192 | Every uuid key is a time-ordered UUIDv7 (bearer handles stay random); each module is re-baselined to one changeset that is edited until the stable release; SchemaLintTest checks keys, foreign-key indexes and column types on the migrated catalog; Liquibase takes a session advisory lock (amends 0008, 0072) |
| 0193 | Metric samples, the message index, broker events, request-reply events and the audit trail are range-partitioned in UTC with no default partition; one maintainer keeps partitions ahead and freezes closed ones; retention detaches concurrently and drops; an audit month drops only when every row is eligible (amends 0134) |
| 0194 | One job runner: a virtual thread per run, the next run after the last ends, a timeout per job, an installation-wide run stops when its lock cannot be extended, a malformed cron is reported and retried, schedules in UTC; replaces 0125's job list (amends 0048) |
| 0195 | A fail-fast connection pool with statement, lock, idle-in-transaction and network timeouts set on every connection; long work raises the statement timeout for its own transaction; the bundled database ships one configuration file, and Studio depends on no server setting |
| 0196 | Heavy endpoints take per-user permits counted across replicas in PostgreSQL, with time and row bounds named in the response; sign-in, upload and in-flight limits are shared counters; a node's call rate is split between ready replicas; long histories page by key |
| 0197 | Each event-stream subscriber holds at most 1,000 frames and 1 MiB; queued change signals coalesce; a subscriber that overflows repeatedly or blocks a write is told to reconnect and replay; broker events are written in commit order (amends 0018) |
| 0198 | A standalone Gatling load test (just load, a manual workflow) drives a replicated Studio and publishes sizing figures with a regression check against docs/sizing.md |
| 0199 | Studio runs on an operator's PostgreSQL 18 in any schema as a non-superuser; a boot preflight refuses old servers, missing privileges and transaction-mode pooling; failover is ridden through; heavy reads may use a lag-checked read replica |
| 0200 | Each tier-C tick reads every listQueues page of every node and then reaps, so a node is fully known one tick after registration; the sweep cursor is gone (amends 0015) |
| 0201 | The product is Rahdar (rah, route + dar, keeper) and the broker SDK is OpenBroker; every identity renamed with no alias, contract 15, a fresh database, the site on rahdar.dev (supersedes 0001 and 0061 D6) |
| 0202 | CodeQL is turned off; SonarQube Cloud is the one static analysis and OSV-Scanner stays for dependencies (supersedes the CodeQL part of 0124) |