diff --git a/AGENTS.md b/AGENTS.md index 3428364e6..3cf586762 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Test: `just test` | Before committing: `just ready` Read @CONTRIBUTING.md first. It covers prerequisites, setup, project structure, -code standards, testing, input validation, and how to add an API domain — all of +code standards, testing, input validation, and how to add an API domain. All of which apply to agents exactly as they apply to people. This file carries only what is specific to agents. @@ -17,7 +17,7 @@ mise exec -- just test `mise` is active in a person's shell and supplies the versions `.mise.toml` declares. An agent's shell has no activation, so a bare `just` resolves to -whatever is installed globally — usually an older version. +whatever is installed globally, usually an older version. The symptom is a check that fails here and passes in continuous integration, on a file nobody edited. When that happens, establish which version ran before @@ -27,13 +27,13 @@ treating the failure as real. `go build ./...` and `go test ./...` fail in this repository. The UI is embedded via `//go:embed dist/*`, which requires `ui/dist/` to hold files at compile -time. Use `just build`, `just test`, or `just ready` — each builds the UI first. +time. Use `just build`, `just test`, or `just ready`. Each builds the UI first. See @CONTRIBUTING.md under "Building and running". ## Where the rules come from @CONTRIBUTING.md names the specification under "Before you start". When a -convention here and the specification disagree, the specification wins — say so +convention here and the specification disagree, the specification wins. Say so rather than following the code. ## Commit trailer diff --git a/CLAUDE.md b/CLAUDE.md index 4365898c8..b337e5b72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code -All agent guidance lives in @AGENTS.md — how to invoke tools, why the Go +All agent guidance lives in @AGENTS.md: how to invoke tools, why the Go toolchain is never called directly, where the rules come from, the commit trailer, and task tracking. This file exists so Claude Code finds it; the content is upstream. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 185739d9e..039d65dc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,26 +10,26 @@ workflow. - Read the [Code of Conduct](CODE_OF_CONDUCT.md). It applies to every interaction in this repo. -- **Design records** — The conventions binding this repository are specified in +- **Design records.** The conventions binding this repository are specified in [osapi-io/specs](https://github.com/osapi-io/specs) under `components/osapi/`, whose `.specify/memory/` is the standing record. Design reasoning for a change - lives there too, not here — a design document kept in this repository goes - stale the moment the code moves past it, with nothing to catch the drift. + lives there, not here. A design document kept in this repository goes stale + the moment the code moves past it, and nothing catches the drift. -- **Get familiar with the project** — Read the docs in this order: +- **Get familiar with the project.** Read the docs in this order: - 1. [Guiding Principles][principles] — design philosophy and project values - 2. [System Architecture][system-architecture] — REST API, NATS, CLI - 3. [API Design Guidelines][api-guidelines] — REST conventions and endpoint + 1. [Guiding Principles][principles]. Design philosophy and project values + 2. [System Architecture][system-architecture]. REST API, NATS, CLI + 3. [API Design Guidelines][api-guidelines]. REST conventions and endpoint structure - 4. [Job System Architecture][job-architecture] — KV-first job processing, + 4. [Job System Architecture][job-architecture]. KV-first job processing, subject routing, and the agent pipeline -- **Check existing work** — Is there an existing PR? Are there issues discussing +- **Check existing work.** Is there an existing PR? Are there issues discussing the feature/change you want to make? Please make sure you consider/address these discussions in your work. -- **Backwards compatibility** — Will your change break existing OSAPI files? It +- **Backwards compatibility.** Will your change break existing OSAPI files? It is much more likely that your change will be merged if it is backwards compatible. Is there an approach you can take that maintains this compatibility? If not, consider opening an issue first so that API changes can @@ -43,17 +43,17 @@ Install tools using [mise]: mise install ``` -- **[Go]** — OSAPI is written in Go. We always support the latest two major Go +- **[Go].** OSAPI is written in Go. We always support the latest two major Go versions, so make sure your version is recent enough. -- **[Node.js]** — Runtime for tools like `@redocly/cli`, for building the +- **[Node.js].** Runtime for tools like `@redocly/cli`, for building the Docusaurus docs site, and for building the embedded React UI in `ui/`. -- **[Bun]** — JavaScript package manager and script runner used for the +- **[Bun].** JavaScript package manager and script runner used for the Docusaurus docs and the React UI. -- **[just]** — Task runner used for building, testing, formatting, and other +- **[just].** Task runner used for building, testing, formatting, and other development workflows. -- **[uv]** — Python package runner. `just md-fmt` formats repository markdown +- **[uv].** Python package runner. `just md-fmt` formats repository markdown with [mdformat] via `uvx`; nothing is installed into the repo. -- **[NATS CLI]** — Command-line tools for interacting with NATS. Useful for +- **[NATS CLI].** Command-line tools for interacting with NATS. Useful for debugging and monitoring during development. Install with `brew install nats-io/nats-tools/nats`. @@ -66,7 +66,7 @@ marketplace: /plugin install commit-commands@claude-plugins-official ``` -- **commit-commands** — provides `/commit` and `/commit-push-pr` slash commands +- **commit-commands.** provides `/commit` and `/commit-push-pr` slash commands that follow the project's commit conventions automatically. **Do not use superpowers.** Spec Kit governs specification, planning, and @@ -86,30 +86,30 @@ just deps ## Project structure -- **`cmd/`** — Cobra CLI commands (`client`, `node agent`, `controller.api`, +- **`cmd/`.** Cobra CLI commands (`client`, `node agent`, `controller.api`, `nats server`). -- **`internal/controller/api/`** — Echo REST API. Node-targeted handlers nest +- **`internal/controller/api/`.** Echo REST API. Node-targeted handlers nest under `node/{domain}/`; controller-only handlers are top-level (`job/`, `health/`). Each domain has its own `gen/` with an OpenAPI spec; the combined spec is `internal/controller/api/gen/api.yaml`. -- **`internal/job/`** — Job domain types and subject routing. `client/` holds - the high-level operations. -- **`internal/agent/`** — Node agent: the consumer/handler/processor pipeline - for job execution. -- **`internal/provider/`** — Operation implementations, organized by category +- **`internal/job/`.** Job domain types and subject routing. `client/` holds the + high-level operations. +- **`internal/agent/`.** Node agent: the consumer/handler/processor pipeline for + job execution. +- **`internal/provider/`.** Operation implementations, organized by category then domain. -- **`internal/config/`** — Viper-based config from `osapi.yaml`. -- **`internal/telemetry/`** — Tracing, metrics, and agent self-metrics. -- **`internal/controller/notify/`** — Condition notification system. -- **`pkg/sdk/`** — Go SDK for programmatic REST API access. -- **`ui/`** — React 19 + TypeScript + Vite + Tailwind CSS v4, embedded into the +- **`internal/config/`.** Viper-based config from `osapi.yaml`. +- **`internal/telemetry/`.** Tracing, metrics, and agent self-metrics. +- **`internal/controller/notify/`.** Condition notification system. +- **`pkg/sdk/`.** Go SDK for programmatic REST API access. +- **`ui/`.** React 19 + TypeScript + Vite + Tailwind CSS v4, embedded into the Go binary at build time. [System Architecture][system-architecture] describes the package layout, handler structure, and provider pattern in full. `nats-client` and `nats-server` are sibling repositories, consumed as pinned -module versions in `go.mod` — there is no `replace` directive. +module versions in `go.mod`. There is no `replace` directive. ## Code style @@ -123,7 +123,7 @@ just go-vet # Run linter ``` The linters that run are declared in `.golangci.yml`. Read them there rather -than looking for a list here — a copied list goes stale the first time the +than looking for a list here. A copied list goes stale the first time the configuration changes. Generated files (`*.gen.go`, `*.pb.go`) are excluded from formatting. @@ -142,7 +142,7 @@ just docusaurus-fmt # Format the site ### Function signatures -Functions with parameters use multi-line format — one parameter per line, with +Functions with parameters use multi-line format, one parameter per line, with the closing parenthesis and the return types on a line of their own: ```go @@ -169,7 +169,7 @@ Name a file for what it holds. Avoid `helpers.go`, `utils.go`, and names of that kind: they describe where code was put rather than what it is, and they accumulate whatever has no other home. -`types.go` holds only type declarations — structs, interfaces, constants, and +`types.go` holds only type declarations: structs, interfaces, constants, and aliases. A function belongs in a file named for what it does. A test file is named for the production file it tests. Where tests grow too @@ -202,7 +202,7 @@ package mocks The generator is resolved through the module's tool dependencies, so every checkout runs the version `go.mod` records. Destination files end in `.gen.go` -and are committed. Do not use `gen/` for mocks — that name is taken by API code +and are committed. Do not use `gen/` for mocks. That name is taken by API code generation. When the interface is **unexported**, a sibling package cannot work: the mock @@ -227,7 +227,7 @@ type. The generated mock is still what satisfies the interface. Three doubles are written by hand, because generating them buys nothing: -- One standing in for a standard library interface — `net.Conn`, `fs.File`, +- One standing in for a standard library interface: `net.Conn`, `fs.File`, `io.Writer`, `slog.Handler`. Those do not move when our code does. - One carrying a real implementation of the behavior under test, such as signing with a genuinely generated key pair. @@ -241,20 +241,20 @@ The conventions below are specific to OSAPI. All logging uses Go's `log/slog` structured logger. -- **Subsystem labels** — Every component that holds a logger MUST wrap it with +- **Subsystem labels.** Every component that holds a logger MUST wrap it with `logger.With(slog.String("subsystem", "..."))` at construction time, which auto-tags every line from that component. Examples: `"agent"`, `"agent.seed"`, `"api.schedule"`, `"provider.file"`, `"job.client"`, `"metrics"`, `"controller.heartbeat"`. -- **Typed attributes** — Use `slog.String("key", val)`, `slog.Int`, `slog.Bool`, +- **Typed attributes.** Use `slog.String("key", val)`, `slog.Int`, `slog.Bool`, `slog.Any`. Never use positional pairs like `"key", val`; they compile but bypass type safety. -- **Standard field names** — `error` for errors, `hostname` for hosts, `path` - for file paths, `job_id` for job IDs, `name` for entry names, `addr` for +- **Standard field names.** `error` for errors, `hostname` for hosts, `path` for + file paths, `job_id` for job IDs, `name` for entry names, `addr` for addresses. -- **Error fields** — `slog.String("error", err.Error())` for string context, or +- **Error fields.** `slog.String("error", err.Error())` for string context, or `slog.Any("error", err)` to preserve the error type. -- **Log levels** — `Debug` for operation dispatch and idempotency skips, `Info` +- **Log levels.** `Debug` for operation dispatch and idempotency skips, `Info` for lifecycle events and state changes, `Warn` for degraded but functional states, `Error` for failures that need attention. @@ -265,7 +265,7 @@ Components use a non-blocking lifecycle: `Start()` returns immediately, and ### Filesystem access -Use [avfs] — `memfs.New()` for in-memory work and `failfs.New()` for targeted +Use [avfs]: `memfs.New()` for in-memory work and `failfs.New()` for targeted error injection. Never use `afero`. ## Testing @@ -285,7 +285,7 @@ just go-unit-cov-check # Report coverage and fail below the target ``` The target is declared in `.github/codecov.yml` and in the shared `go` justfile -module — change both together. +module. Change both together. ### Test file conventions @@ -296,7 +296,7 @@ module — change both together. - Suite naming: `*_public_test.go` → `{Name}PublicTestSuite`, `*_test.go` → `{Name}TestSuite`. - `testify/suite` with table-driven cases. -- One suite method per function under test — success, errors, and edge cases are +- One suite method per function under test. Success, errors, and edge cases are rows in one table, not separate methods. - `export_test.go` exposes unexported symbols to external tests, by alias or by setter. Do not use an alias to re-cover behavior the caller's own test already @@ -304,11 +304,11 @@ module — change both together. ### Test layers -- **Unit tests** (`*_test.go`, `*_public_test.go`) — fast, mocked dependencies. +- **Unit tests** (`*_test.go`, `*_public_test.go`). Fast, mocked dependencies. Public suites also carry HTTP wiring methods (`TestXxxHTTP`, `TestXxxRBACHTTP`) that send raw HTTP through the full Echo middleware stack with mocked backends. -- **Integration tests** (`test/integration/`) — build and start a real `osapi` +- **Integration tests** (`test/integration/`). Build and start a real `osapi` binary and exercise CLI commands end-to-end. Guarded by a `//go:build integration` tag. The harness allocates random ports, generates a JWT, and starts the server, so no external setup is required. @@ -390,7 +390,7 @@ if errMsg, ok := validation.Struct(request.Body); !ok { ``` This applies to action endpoints (power, docker stop) where an empty body is -valid — unlike update endpoints, which must use `AtLeastOneField`. +valid, unlike update endpoints, which must use `AtLeastOneField`. ## Adding a new API domain @@ -463,13 +463,13 @@ be reasonable to split it in a few). Git squash and rebase is your friend! ## Submitting a PR -- **Describe your changes** — Ensure that you provide a comprehensive - description of your changes. -- **Issue/PR links** — Link any previous work such as related issues or PRs. +- **Describe your changes.** Say what changed and why. A reviewer should not + have to read the diff to learn the reason for it. +- **Issue/PR links.** Link any previous work such as related issues or PRs. Please describe how your changes differ to/extend this work. -- **Examples** — Add any examples or screenshots that you think are useful to +- **Examples.** Add any examples or screenshots that you think are useful to demonstrate the effect of your changes. -- **Draft PRs** — If your changes are incomplete, but you would like to discuss +- **Draft PRs.** If your changes are incomplete, but you would like to discuss them, open the PR as a draft and add a comment to start a discussion. Using comments rather than the PR description allows the description to be updated later while preserving any discussions. @@ -477,7 +477,7 @@ be reasonable to split it in a few). Git squash and rebase is your friend! ## AI usage This repo is written with AI assistance. All contributions are subject to the -[AI Usage Policy](AI_POLICY.md) — disclose the tool you used, and make sure you +[AI Usage Policy](AI_POLICY.md). Disclose the tool you used, and make sure you can explain what your change does without the aid of AI tools. ## FAQ @@ -490,7 +490,7 @@ answer questions. > I'm stuck, where can I get help? -If you have questions, feel free to open a [Discussion] on GitHub. +If you have questions, open a [Discussion] on GitHub. [adding-an-api-domain]: docs/docs/sidebar/development/adding-an-api-domain.md [api-guidelines]: docs/docs/sidebar/architecture/api-guidelines.md diff --git a/README.md b/README.md index 14563ebda..5de971d95 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,11 @@ them to be used as appliances. OSAPI demo -## 📖 Documentation +## Documentation [Getting Started] | [API] | [Usage] | [SDK] -## 🔗 Sister Projects +## Sister projects | Project | Description | | -------------------- | ------------------------------------------------------------------- | @@ -39,12 +39,12 @@ them to be used as appliances. | [nats-server] | A Go package for running an embedded NATS server | | [osapi-orchestrator] | Declarative infrastructure orchestration DSL built on the OSAPI SDK | -## 🤝 Contributing +## Contributing See the [Contributing](CONTRIBUTING.md) guide for prerequisites, setup, conventions, and the PR workflow. -## 📄 License +## License The [MIT] License. diff --git a/ui/README.md b/ui/README.md index 4776aa569..e41a121fa 100644 --- a/ui/README.md +++ b/ui/README.md @@ -19,7 +19,7 @@ language. Configure

-## ✨ Features +## Features | Feature | Description | | ----------------- | ----------------------------------------------------------------------------------------------- | @@ -30,13 +30,13 @@ language. | @fact. References | Auto-complete fact references in DNS and network fields from live API | | Generated SDK | Typed fetch functions from OSAPI's OpenAPI spec via [orval](https://orval.dev/) | -## 🤝 Contributing +## Contributing See the [Development](docs/development.md) guide for prerequisites, setup, and conventions. See the [Contributing](docs/contributing.md) guide before submitting a PR. -## 📄 License +## License The [MIT] License. diff --git a/ui/docs/architecture.md b/ui/docs/architecture.md index cb1f6cbf5..c35289857 100644 --- a/ui/docs/architecture.md +++ b/ui/docs/architecture.md @@ -3,19 +3,19 @@ OSAPI UI is a React single-page application that provides a management dashboard and operations builder for [OSAPI](https://github.com/osapi-io/osapi). -## Tech Stack +## Tech stack -- **React 19** — UI framework -- **TypeScript** — type-safe codebase -- **Vite** — build tool and dev server -- **Tailwind CSS v4** — utility-first styling via CSS-based `@theme` -- **class-variance-authority (cva)** — component variant patterns -- **clsx + tailwind-merge** — conditional class composition (`cn()` helper) -- **lucide-react** — icons -- **React Router v7** — client-side routing -- **orval** — OpenAPI SDK generation +- **React 19**. UI framework +- **TypeScript**. Type-safe codebase +- **Vite**. Build tool and dev server +- **Tailwind CSS v4**. Utility-first styling via CSS-based `@theme` +- **class-variance-authority (cva)**. Component variant patterns +- **clsx + tailwind-merge**. Conditional class composition (`cn()` helper) +- **lucide-react**. Icons +- **React Router v7**. Client-side routing +- **orval**. OpenAPI SDK generation -## Application Structure +## Application structure ``` src/ @@ -45,9 +45,9 @@ src/ pages/ — Route page components ``` -## Authentication & Authorization +## Authentication & authorization -### Auth Flow +### Auth flow ``` User opens app @@ -59,18 +59,18 @@ User opens app → Auto-authenticate → Dashboard ``` -### JWT Token +### JWT token OSAPI issues JWTs via `osapi token generate`. The token contains: -- `roles` claim — array of role strings (`admin`, `write`, `read`) +- `roles` claim, array of role strings (`admin`, `write`, `read`) - Standard JWT claims (sub, exp, iat) -The UI decodes the token client-side (no verification — that's the server's job) +The UI decodes the token client-side (no verification, that's the server's job) to extract roles. The token is sent as a Bearer header on every API request via the fetch mutator. -### RBAC Model +### RBAC model Three built-in roles with hierarchical permissions: @@ -83,20 +83,20 @@ Three built-in roles with hierarchical permissions: Permissions use `resource:verb` format matching osapi's Go model: `agent:read`, `file:write`, `command:execute`, `docker:execute`, etc. -### Permission Gating +### Permission gating -- **Configure blocks** — each block type maps to a required permission in +- **Configure blocks**. Each block type maps to a required permission in `BLOCK_PERMISSIONS`. Unauthorized blocks show greyed out with a lock icon. -- **Agent drain/undrain** — requires `agent:write`. -- **Role override dropdown** — lets users preview what other roles see without +- **Agent drain/undrain**. Requires `agent:write`. +- **Role override dropdown**. Lets users preview what other roles see without changing the token. -## SDK Generation +## SDK generation The TypeScript SDK is generated from OSAPI's combined OpenAPI spec using [orval](https://orval.dev/). -### Generation Flow +### Generation flow ``` osapi repo osapi-ui repo @@ -116,7 +116,7 @@ osapi repo osapi-ui repo `src/sdk/gen/api.yaml` and runs orval to produce typed fetch functions and schema types. -### Fetch Mutator +### Fetch mutator `src/sdk/fetch.ts` is the only hand-written file in `src/sdk/`. It: @@ -125,7 +125,7 @@ osapi repo osapi-ui repo - Sends it as a `Bearer` header - Wraps responses in `{ data, status, headers }` for orval -## Component Architecture +## Component architecture ### Layers @@ -136,45 +136,45 @@ Pages (Dashboard, Configure, Roles, SignIn) └─ Tailwind CSS theme tokens ``` -### UI Primitives (`src/components/ui/`) +### UI primitives (`src/components/ui/`) Framework-level reusable components. These define the visual language: -| Component | Purpose | -| ---------------------------------------- | -------------------------------------------------------------- | -| Text | Styled text with variant/size props — the default for all text | -| Card, CardHeader, CardTitle, CardContent | Container with variant borders/shadows | -| Button | Primary/secondary/ghost/destructive with sizes | -| Badge | Status indicators (ready/pending/running/error/applied/muted) | -| Input | Form input with label, autofill suppression | -| Dropdown | Custom popover dropdown (replaces native select) | -| FactInput | Input with @fact. reference autocomplete | -| FormField | Label + Input wrapper for consistent form layout | -| PageHeader | Page title + subtitle + optional actions slot | -| SectionLabel | Uppercase section header with optional icon | -| StatCard | Label + big value + detail text in a card | -| DataTable | Typed table with header/rows inside a card | -| HealthDot | Colored status dot (ok/error/muted) | -| ErrorBanner | Error message with icon (sm/md sizes) | -| MetricValue | Formatted metric display (label + value) | -| CodeBlock | Styled code/pre block with border | -| IdBadge | Monospace ID pill for identifiers | -| IconButton | Icon-only button with ghost/danger/accent variants | -| CollapsibleSection | Togglable section with chevron, icon, right content | -| Modal | Dialog overlay with close button | -| EmptyState | Centered message with dashed border and optional icon | -| SearchBox | Inline search input with close button | -| ScrollButton | Directional scroll arrow (left/right) | -| InfoBox | Subtle container for hints and info text | -| KeyValue | Inline key:value display pair | -| LabelTag | Accent-colored key:value label pill | -| ConditionAlert | Warning condition with triangle icon | -| Popover, PopoverItem, PopoverPanel | Floating popover menu system | - -### Text Component +| Component | Purpose | +| ---------------------------------------- | ------------------------------------------------------------- | +| Text | Styled text with variant/size props, the default for all text | +| Card, CardHeader, CardTitle, CardContent | Container with variant borders/shadows | +| Button | Primary/secondary/ghost/destructive with sizes | +| Badge | Status indicators (ready/pending/running/error/applied/muted) | +| Input | Form input with label, autofill suppression | +| Dropdown | Custom popover dropdown (replaces native select) | +| FactInput | Input with @fact. reference autocomplete | +| FormField | Label + Input wrapper for consistent form layout | +| PageHeader | Page title + subtitle + optional actions slot | +| SectionLabel | Uppercase section header with optional icon | +| StatCard | Label + big value + detail text in a card | +| DataTable | Typed table with header/rows inside a card | +| HealthDot | Colored status dot (ok/error/muted) | +| ErrorBanner | Error message with icon (sm/md sizes) | +| MetricValue | Formatted metric display (label + value) | +| CodeBlock | Styled code/pre block with border | +| IdBadge | Monospace ID pill for identifiers | +| IconButton | Icon-only button with ghost/danger/accent variants | +| CollapsibleSection | Togglable section with chevron, icon, right content | +| Modal | Dialog overlay with close button | +| EmptyState | Centered message with dashed border and optional icon | +| SearchBox | Inline search input with close button | +| ScrollButton | Directional scroll arrow (left/right) | +| InfoBox | Subtle container for hints and info text | +| KeyValue | Inline key:value display pair | +| LabelTag | Accent-colored key:value label pill | +| ConditionAlert | Warning condition with triangle icon | +| Popover, PopoverItem, PopoverPanel | Floating popover menu system | + +### Text component The `Text` component is the standard way to render styled text. Never write -inline Tailwind text classes — always use `Text` with the appropriate variant: +inline Tailwind text classes, always use `Text` with the appropriate variant: ```tsx Secondary text @@ -189,18 +189,18 @@ Available variants: `default`, `muted`, `label`, `mono`, `mono-muted`, Available sizes: `xs` (default), `sm`, `base`. -### Domain Components (`src/components/domain/`) +### Domain components (`src/components/domain/`) Business logic components specific to OSAPI: -- **Block system** — BlockCard, BlockStack, ApplyButton, ResultCard, +- **Block system**. BlockCard, BlockStack, ApplyButton, ResultCard, SaveStackDialog, StackBar -- **Block forms** — one per operation type (CommandBlock, CronBlock, - DockerBlock, DockerExecBlock, FileBlock, FileUploadBlock, FileDeleteBlock, - CronDeleteBlock, ContainerActionBlock, DnsUpdateBlock, SingleInputBlock) -- **Pickers** — TargetPicker (agents/labels), ObjectPicker (files), +- **Block forms**. One per operation type (CommandBlock, CronBlock, DockerBlock, + DockerExecBlock, FileBlock, FileUploadBlock, FileDeleteBlock, CronDeleteBlock, + ContainerActionBlock, DnsUpdateBlock, SingleInputBlock) +- **Pickers**. TargetPicker (agents/labels), ObjectPicker (files), ContainerPicker, CronPicker -- **Dashboard** — AgentCard, ComponentRow, JobDetail, HostGroupHeader +- **Dashboard**. AgentCard, ComponentRow, JobDetail, HostGroupHeader ### Hooks (`src/hooks/`) @@ -214,7 +214,7 @@ Business logic components specific to OSAPI: | useObjects | Fetch file objects for pickers | | useFacts | Fetch @fact. keys from `/facts/keys` | -## Feature Flags +## Feature flags Feature flags gate unreleased functionality via env vars: