docs: track the coding guidelines and finish tree hygiene
Some checks failed
Deploy Website / deploy (push) Successful in 2m7s
Publish Edge Images / images (apps/console/Dockerfile, barkstack) (push) Successful in 12m9s
Publish Edge Images / images (services/pawsql/Dockerfile, pawsql) (push) Successful in 13m7s
Test and Release Barkstack / test (push) Failing after 2m32s
Verify Platform / verify (push) Failing after 48s
Verify Platform / images (apps/console/Dockerfile, console) (push) Has been skipped
Verify Platform / images (services/pawsql/Dockerfile, pawsql) (push) Has been skipped
Verify Platform / images (services/treatvault/Dockerfile, treatvault) (push) Has been skipped
Test and Release Barkstack / release (push) Has been skipped
Publish Edge Images / images (services/treatvault/Dockerfile, treatvault) (push) Successful in 8m50s

AGENT.md now points at .ai/guidelines, so the guidelines move into the
repository and the reference resolves on a fresh clone. Completes the
exported doc comments in the console manifest and registry left from
the lint pass, and ignores local scratch files (.DS_Store, sdkscratch).

The root Barkfile's include files (Barkfile.campbellwireless,
Barkfile.ruckstack) stay local on purpose: they are personal
infrastructure and Barkfile.campbellwireless currently does not parse
(a treatvault block inside a project block, which the parser rejects).
This commit is contained in:
2026-09-19 18:58:08 -04:00
parent 49c80ee82f
commit ee80e3322f
8 changed files with 1430 additions and 5 deletions

View File

@@ -0,0 +1,250 @@
# Barkstack Conventions
Barkstack is a self-hosted server stack intended for homelabbers. The idea is to provide something akin to AWS, Azure or GCP but on a much smaller scale for homelabbers.
The intent is that the platform is modular, that the user can enable and disable features at will and that we make the best use of resources as possible.
## 1. Portability
Barkstack should model infrastructure in terms of Barkstack concepts rather than the implementation details of the tools used underneath it.
Where Barkstack relies on external systems such as Docker, Docker Swarm, CoreDNS, ACME providers, or storage backends, access to those systems should be defined through stable interfaces with concrete provider implementations.
Runtime-specific types and assumptions should not leak into the core Barkstack resource model.
For example, an application should be described as a Barkstack workload rather than a Docker service, and a network should be described in terms of the capabilities Barkstack requires rather than as a Docker overlay network.
This should allow Barkstack to support alternative implementations in the future without requiring the higher-level resource model or Barkfiles to be redesigned.
Provider-specific functionality may be exposed where useful, but it should be treated as an explicit extension rather than the foundation of the platform.
## 2. Modularity
A user should be able to run only the functionality they need. For example, Barkstack must not require PawSQL, TreatVault, DigDNS, CollarCA, or any other optional module simply in order for the core platform to operate.
Modules should communicate through well-defined Barkstack contracts rather than depending directly on another module's internal implementation.
Disabling or removing a module should only disable the capabilities provided by that module and features that explicitly depend on it. It should not unnecessarily prevent unrelated parts of Barkstack from operating.
Modules should own their implementation, API, lifecycle, and management UI while integrating into the common Barkstack control plane and console.
## 3. Reproducability
A Barkstack installation should be reproducible from its declarative configuration and backed-up user data.
The Barkfile is the source of truth for user-controlled desired state. Resources and configuration must not exist solely as hidden state inside the Barkstack database, web console, or runtime.
Changes made through the CLI, API, or management console should ultimately be reflected in the same declarative resource model represented by the Barkfile.
Barkstack should clearly separate:
Desired configuration — what the user wants to exist, represented by the Barkfile.
Runtime state — information Barkstack discovers or records about the running system, such as IDs, placement, health, status, and timestamps.
User data — persistent application data, database contents, volumes, secrets, certificates, and other state that cannot be recreated purely from configuration.
A user should be able to move Barkstack to a new system by restoring their Barkfile and required user data, then allowing Barkstack to reconcile the new environment back to the desired state.
Barkstack should provide or integrate with mechanisms for backing up and restoring user data separately from configuration.
## 4. Declarative Desired State
Barkstack configuration describes the state the user wants, not a sequence of actions to execute.
Components should reconcile the current state of the system against the desired state expressed in the Barkfile.
Running the same configuration repeatedly should be safe and idempotent.
Where possible, Barkstack should be able to explain the difference between current and desired state before applying changes.
The parser, resource model, planning/reconciliation logic, and runtime implementation should remain separate concerns.
## 5. No Hidden Configuration
The Barkfile is the source of truth for user-controlled configuration.
The management UI, CLI, and APIs must not create configuration that exists only inside Barkstack's internal database.
Changes made through the UI should ultimately result in the same declarative configuration that could have been written by hand.
Internal state may record runtime information such as resource IDs, health, placement, timestamps, or observed state, but must not silently become an alternative source of desired configuration.
## 6. Module Independence
Modules must not unnecessarily depend on other Barkstack modules.
For example, PawSQL should not require DigDNS, CollarCA or TreatVault unless a feature explicitly uses them.
Modules should interact through stable Barkstack contracts rather than importing or depending directly on another module's implementation.
Removing or disabling one module should degrade related functionality cleanly rather than prevent Barkstack from operating.
## 7. Project Isolation
Projects are the normal namespace and isolation boundary for user workloads and resources.
Resources belonging to one project must not implicitly gain access to resources belonging to another project.
Cross-project communication must be explicit.
Cluster-level Barkstack services may bridge project boundaries only where required by their function.
Isolation should be enforced using the strongest practical mechanism provided by the underlying runtime, such as separate Docker Swarm overlay networks.
## 8. Least Privilege
Every Barkstack component should receive only the permissions, secrets, networks, volumes and runtime access required to perform its function.
Access to privileged facilities such as the Docker socket, Swarm manager API, certificate authority keys or secrets stores should be restricted to the smallest possible number of components.
A module should request an operation from the Barkstack control plane rather than receiving broad infrastructure access whenever practical.
Secrets must never be passed through command-line arguments, logs or ordinary environment variables when a more appropriate secret mechanism exists.
## 9. Secure Defaults
The default configuration should favour isolation and safety.
Examples include:
- project networks should not be attachable unless requested
- services should not publish ports unless explicitly exposed
- secrets should only be attached to services that reference them
- databases should not expose their native ports merely for internal connectivity
- unknown routes, hosts or resources should fail closed rather than fall back to a default
Users may explicitly relax these restrictions when necessary.
## 10. Resource Efficiency
Barkstack is intended for homelabs where CPU, RAM and storage may be limited.
Modules should avoid permanently running processes when they can safely be suspended, shared or started on demand.
Prefer one cluster-level service where appropriate rather than unnecessary per-project copies.
Resource usage should be observable, and background functionality should justify the resources it consumes.
Efficiency must not come at the expense of data integrity.
## 11. Control Plane vs Workloads
Barkstack's control plane and the workloads it manages should remain conceptually separate.
The Barkstack controller must not depend on a user workload that it is itself responsible for managing in order to become operational.
Bootstrap-critical functionality should have minimal dependencies.
Avoid circular dependencies such as Barkstack requiring a PawSQL-managed database before PawSQL can be started by Barkstack.
## 12. Stable Resource Identity
Resources should have stable identities independent of their current runtime placement.
Applications should refer to logical resources rather than physical nodes, container IDs or IP addresses wherever possible.
Moving a database, application or service between nodes should not require dependent resources to be reconfigured.
Friendly names may be scoped by project, but internally Barkstack should be able to uniquely identify resources without ambiguity.
## 13. Runtime Details Are Implementation Details
The Barkfile should describe user intent rather than leaking unnecessary details of Docker, Swarm, Caddy or other runtime technologies.
For example, users should normally describe:
visibility public
rather than:
publish_mode ingress
and:
project foo
rather than manually constructing overlay networks.
Runtime-specific escape hatches may exist for advanced users, but they should not define the core Barkstack resource model.
## 14. Graceful Degradation
The failure or absence of one module should not unnecessarily take down the rest of Barkstack.
If PawSQL is unavailable, the Barkstack console, DNS management and certificate management should remain usable.
The UI and CLI should clearly distinguish between:
- unavailable
- degraded
- unhealthy
- intentionally disabled
Failures should be visible and actionable rather than silently ignored.
## 15. Data Safety
Barkstack must never sacrifice user data for convenience or automatic recovery.
If Barkstack cannot safely determine where persistent data resides, it should fail rather than start a service against an empty or incorrect volume.
Destructive operations should be explicit.
Persistent data lifecycle must be separate from workload lifecycle; deleting or recreating a container or service must not implicitly delete its data.
Backup and restore should be considered part of the resource lifecycle, not an afterthought.
## 16. Observable Behaviour
Barkstack should make it possible to understand what it is doing and why.
Important operations should produce structured logs and useful events.
Where appropriate, resources should expose:
- desired state
- observed state
- health
- current placement
- dependencies
- last meaningful event
- errors preventing reconciliation
Users should not need to inspect Docker internals for normal troubleshooting.
## 17. Predictability Over Magic
Automatic behaviour should be deterministic and explainable.
Barkstack may choose placement, generate names, provision networks, issue certificates or suspend resources automatically, but the rules used to make those decisions should be understandable and inspectable.
Avoid behaviour that changes depending on undocumented heuristics.
If Barkstack makes a decision on behalf of the user, the UI and CLI should make that decision visible.
## 18. Backwards-Compatible Evolution
Barkfiles are user-owned infrastructure definitions and should be treated as a stable public interface.
Avoid breaking syntax or semantics unnecessarily.
When a breaking change is unavoidable, provide a clear migration path and useful validation errors.
Configuration formats and module APIs should be versioned where doing so protects future compatibility.
## 19. One Resource Model, Multiple Interfaces
The Barkfile, CLI, web console and APIs should operate against the same underlying Barkstack resource model.
Do not implement separate business logic for each interface.
A database created through the web console should represent the same resource as one declared through the Barkfile.
The UI is a frontend to Barkstack, not a second control plane.
## 20. Self-Hosted First
Barkstack must not require a third-party cloud service to provide its core functionality.
External services may be supported as optional providers, but there should be a fully self-hosted path for core functionality wherever reasonably possible.
Barkstack should continue to function on an isolated LAN except for features that inherently require external connectivity.

View File

@@ -0,0 +1,513 @@
# Go Coding Conventions
Barkstack Go code should favour clarity, correctness, maintainability, and idiomatic Go over abstraction or cleverness.
Code should be easy for another developer to understand without needing to reconstruct the author's intent.
## 1. General Style
All Go code must be formatted with `gofmt`.
Prefer standard, idiomatic Go patterns over custom frameworks or abstractions.
Use the Go standard library where it provides a reasonable solution. Introduce third-party dependencies only when they provide meaningful value.
Prefer simple code that is easy to delete, replace, or refactor.
Avoid speculative abstractions for functionality that does not yet exist.
Do not introduce design patterns simply because they are common in other languages.
## 2. Packages
Packages should have a clear and narrow responsibility.
Prefer package names that describe what the package provides:
```text
config
router
runtime
projects
secrets
network
```
Avoid vague package names such as:
```text
utils
helpers
common
misc
manager
```
unless the name genuinely describes a coherent domain concept.
Avoid circular dependencies.
Higher-level Barkstack domain packages must not depend directly on implementation-specific packages where an abstraction boundary exists.
For example:
```text
projects
runtime.Runtime
runtime/swarm
```
is preferable to:
```text
projects
docker.Client
```
## 3. Interfaces
Interfaces should represent meaningful behavioural boundaries.
Prefer defining interfaces where they are consumed rather than creating large provider interfaces pre-emptively.
Interfaces should normally be small.
For example:
```go
type SecretStore interface {
Get(ctx context.Context, name string) ([]byte, error)
}
```
is preferable to one large interface exposing every possible secrets operation.
Do not create an interface solely to make every struct mockable.
Use concrete types where no abstraction boundary is required.
Implementation-specific types such as Docker, Swarm, CoreDNS, or provider SDK types should not leak through Barkstack domain interfaces.
## 4. Types and Resource Models
Use explicit domain types for important Barkstack concepts.
Prefer:
```go
type ProjectID string
type ResourceID string
```
where stronger typing helps prevent accidental misuse.
Avoid representing structured domain data as untyped maps such as:
```go
map[string]any
```
unless working at a serialization or compatibility boundary.
Resource models should distinguish between:
* desired state
* observed/runtime state
* provider-specific state
Do not mix all three into one large struct without a clear reason.
## 5. Functions
Functions should do one understandable job.
Prefer short functions with descriptive names over long functions containing multiple phases of behaviour.
If a function requires extensive comments explaining its control flow, consider splitting it.
Avoid boolean parameters whose meaning is unclear:
```go
createThing(true, false)
```
Prefer explicit options or separate methods.
Return early where doing so reduces nesting.
Prefer:
```go
if err != nil {
return err
}
if !enabled {
return nil
}
```
over deeply nested conditionals.
## 6. Errors
Errors must be handled explicitly.
Do not silently discard errors unless there is a documented reason.
Wrap errors with useful context using `%w`:
```go
return fmt.Errorf("create project network %q: %w", name, err)
```
Error messages should explain what operation failed and identify the relevant resource where useful.
Avoid redundant wording such as:
```text
error: failed to...
```
when the caller already knows it received an error.
Use sentinel errors or typed errors when callers need to distinguish classes of failure programmatically.
Do not inspect error strings to determine behaviour.
Expected user-facing validation failures should be distinguishable from unexpected internal failures.
## 7. Context
Operations involving I/O, networking, subprocesses, Docker/Swarm APIs, databases, or potentially long-running work should accept `context.Context`.
`context.Context` should normally be the first function parameter:
```go
func Resolve(ctx context.Context, name string) (Resource, error)
```
Do not store contexts inside long-lived structs.
Respect cancellation and deadlines.
Where appropriate, apply explicit timeouts around external systems rather than allowing operations to wait indefinitely.
## 8. Concurrency
Do not introduce concurrency unless it provides a meaningful benefit.
Concurrency must have a clear ownership and cancellation model.
Every goroutine should have an understandable lifetime.
Avoid goroutines that can outlive the component that created them unless intentionally managed as background workers.
Use contexts, channels, `sync.WaitGroup`, `errgroup`, or equivalent mechanisms to coordinate shutdown.
Avoid shared mutable state where message passing or immutable snapshots are simpler.
Run tests with the race detector where concurrency is involved:
```bash
go test -race ./...
```
## 9. Resource Cleanup
Resources must be released deterministically.
Use `defer` for cleanup where appropriate:
```go
conn, err := net.Dial(...)
if err != nil {
return err
}
defer conn.Close()
```
Pay particular attention to:
* network connections
* HTTP response bodies
* files
* temporary directories
* goroutines
* timers
* Docker/API clients
Do not leak resources on error paths.
## 10. Configuration Parsing and Validation
Parsing and validation should be separate concerns.
The parser should answer:
> What configuration did the user write?
Validation should answer:
> Is this configuration valid?
Runtime reconciliation should answer:
> How do we make the system match this configuration?
Avoid performing Docker, filesystem, network, or other runtime operations while parsing the Barkfile.
Validation errors should include useful source context such as:
* filename
* line number
* resource name
* field name
where available.
Prefer collecting multiple independent validation failures when doing so helps the user fix configuration efficiently.
## 11. Reconciliation
Reconciliation logic should be idempotent.
Running reconciliation repeatedly against an already-correct system should result in no destructive or unnecessary changes.
Prefer:
```text
observe
compare
plan
apply
```
over blindly issuing create/update commands.
Reconciliation should clearly distinguish:
* resource does not exist
* resource already matches desired state
* resource requires modification
* resource cannot safely be reconciled
Never destroy or recreate persistent resources merely because doing so is easier to implement.
## 12. Logging
Use structured logging.
Prefer `log/slog` unless the project has deliberately standardized on another logger.
Logs should contain useful fields:
```go
logger.Info(
"database started",
"project", projectName,
"database", databaseName,
"node", nodeName,
)
```
Avoid encoding structured information into formatted message strings.
Do not log:
* passwords
* private keys
* secret contents
* authentication tokens
* full connection strings containing credentials
Use appropriate log levels consistently.
Expected user mistakes should generally not produce alarming internal error logs.
## 13. Naming
Use clear, descriptive names.
Avoid abbreviations unless they are broadly understood within the domain.
Prefer:
```go
database
project
hostname
certificate
```
over:
```go
db
proj
host
cert
```
when the longer name improves readability.
Short variable names are appropriate for very small scopes where their meaning is obvious.
Exported identifiers must have meaningful names and should form readable API surfaces.
## 14. Comments and Documentation
Comments should explain why something exists or why a non-obvious decision was made.
Do not write comments that simply repeat the code.
Prefer:
```go
// Keep the database pinned to this node because its volume is node-local.
```
over:
```go
// Set node ID.
```
Exported public APIs should have Go documentation comments where appropriate.
Important architectural constraints should be documented close to the relevant interface or implementation.
## 15. Dependency Injection
Dependencies should be explicit.
Prefer constructor injection:
```go
func NewController(runtime Runtime, secrets SecretStore) *Controller
```
over hidden global state or package-level mutable variables.
Avoid service locator patterns.
Components should be testable without requiring a live Docker daemon, DNS server, or Swarm cluster unless the test is explicitly an integration test.
## 16. Global State
Avoid mutable global state.
Constants, immutable lookup tables, embedded static files, and similar read-only data are acceptable.
Runtime clients, configuration, loggers, caches, and mutable registries should normally belong to explicitly constructed objects.
## 17. Testing
New behaviour must include tests appropriate to its risk.
Use table-driven tests where they improve clarity.
Test observable behaviour rather than implementation details.
Unit tests should cover:
* normal operation
* invalid input
* boundary conditions
* important error paths
Use `httptest`, temporary directories, fake implementations, and in-memory resources where appropriate.
Integration tests should be used for behaviour that depends on real external systems such as:
* Docker Swarm
* PostgreSQL
* DNS
* filesystem behaviour
* certificate issuance
Tests must not depend on execution order.
Tests should clean up resources they create.
## 18. Test Fixtures and Mocks
Prefer lightweight fakes or small test implementations over large generated mocks where practical.
A fake should implement the same public interface as the production provider.
Do not make production interfaces artificially large simply to support mocking.
Shared test fixtures should remain readable and should not obscure what each test is actually proving.
## 19. Validation Before Completion
Before considering Go changes complete, run at minimum:
```bash
gofmt -w .
go test ./...
go vet ./...
```
Where concurrency is involved:
```bash
go test -race ./...
```
Run any project-configured linters as well.
The repository should compile cleanly and tests should pass before work is considered finished.
Do not leave known lint, test, or compilation failures for later.
## 20. Backwards Compatibility
Treat public Go interfaces, Barkfile parsing behaviour, APIs, and persisted resource formats as compatibility surfaces.
Avoid breaking existing behaviour without a concrete reason.
When changing public behaviour:
* update tests
* update documentation
* provide migration handling where necessary
Do not silently reinterpret existing configuration.
## 21. Security
Treat all external input as untrusted, including:
* Barkfiles
* API requests
* DNS names
* resource names
* provider responses
* filesystem paths
Validate input at boundaries.
Avoid shelling out where a native Go API exists.
Where subprocess execution is necessary, never build commands by concatenating untrusted strings.
Use explicit permissions when creating files containing sensitive information.
Security-sensitive operations should fail closed.
## 22. Performance
Optimize only where there is evidence that optimization matters.
Avoid unnecessary polling, background workers, allocations, or repeated API calls.
Cache external state only when there is a clear invalidation model.
For homelab use, reducing persistent CPU and memory consumption is generally more valuable than maximizing throughput.
Correctness and data safety take priority over performance.

View File

@@ -0,0 +1,630 @@
# Svelte Coding Conventions
Barkstack frontend code should be simple, strongly typed, accessible, modular, and consistent across the core console and independently shipped module UIs.
Use Svelte 5 patterns and TypeScript for application code.
The UI should favour clarity and predictable behaviour over clever abstractions or excessive frontend framework machinery.
## 1. TypeScript
Use TypeScript for Svelte components and frontend application code.
Prefer explicit types for:
* API responses
* resource models
* component props
* event payloads
* configuration objects
Avoid `any`.
Where data originates outside the frontend's trust boundary, validate or narrow it before treating it as a trusted domain type.
Do not duplicate backend models blindly if the frontend only requires a smaller view model.
## 2. Svelte 5
Use modern Svelte 5 patterns.
Prefer runes such as:
```text
$state
$derived
$effect
```
where reactive state is required.
Do not introduce legacy reactive syntax into new code unless required by compatibility with existing code.
Use `$effect` for actual side effects, not as a substitute for derived state.
Prefer `$derived` when a value can be calculated directly from other state.
Keep reactive data flow understandable.
## 3. Components
Components should have one clear responsibility.
Prefer composition of small components over very large page components.
A component that simultaneously:
* fetches data
* manages routing
* contains multiple forms
* renders several unrelated panels
* handles dialogs
should usually be decomposed.
Do not extract tiny components merely for the sake of having more files.
Create a component when it provides a meaningful reusable or conceptual boundary.
## 4. Props
Component inputs should be explicit and strongly typed.
Prefer a clear props interface over passing large unstructured objects.
Do not mutate props owned by a parent.
Use callbacks or application events for actions that belong to the parent.
Default values should be obvious and predictable.
## 5. State Ownership
State should live at the narrowest scope that needs it.
Prefer:
```text
component-local state
```
over:
```text
page state
```
and page state over:
```text
global state
```
unless multiple unrelated areas genuinely need to share the state.
Avoid global stores for ordinary component state.
Do not create global state merely to avoid passing a small number of props.
## 6. Derived State
Do not store data that can be reliably derived from existing state.
Prefer:
```ts
let filtered = $derived(items.filter(...));
```
over maintaining both:
```text
items
filteredItems
```
through manual synchronization.
Duplicated state is a common source of frontend bugs.
## 7. Effects
Effects should be used sparingly.
An `$effect` should normally interact with something outside normal declarative state flow, such as:
* browser APIs
* subscriptions
* timers
* imperative third-party libraries
Do not use effects merely to copy one reactive value into another.
Effects that allocate resources must clean them up.
## 8. API Access
Centralize HTTP/API behaviour rather than scattering raw `fetch` calls throughout components.
Prefer:
```text
API client
page/component
```
over embedding URL construction, error parsing, authentication handling, and response decoding in every component.
API calls should:
* handle non-success responses
* expose meaningful typed errors
* support cancellation where appropriate
* avoid silently swallowing failures
Use relative API URLs so Barkstack remains independent of its public hostname or reverse-proxy configuration.
## 9. Loading, Empty and Error States
Every asynchronous screen should deliberately handle:
* loading
* success
* empty
* error
Do not leave a blank screen while loading.
Do not treat an empty resource list as an error.
Errors should explain what failed and, where practical, what the user can do next.
Avoid exposing raw stack traces or backend implementation details to ordinary users.
## 10. Routing
The Barkstack shell owns global browser routing.
Module UIs should operate within the route namespace assigned to them.
A module must not assume control over the entire browser application.
For plugin/module navigation, use the Barkstack navigation contract rather than directly replacing global application state.
Deep links, browser refresh, back, and forward navigation should work.
Do not encode critical application state solely in transient component memory when it belongs in the URL.
## 11. Barkstack Module Boundaries
A module such as PawSQL, DigDNS, CollarCA, or TreatVault owns its module-specific UI.
The Barkstack core console owns shared application chrome such as:
* sidebar
* global header
* routing
* breadcrumbs
* notifications
* confirmation dialogs
* global theme
Module UIs must not recreate these elements.
Module UI code should not be copied into the Barkstack core repository merely for convenience.
Keep the plugin contract independent of a specific module.
## 12. Custom Elements
Where Barkstack modules are loaded as custom elements, treat the custom-element boundary as a public compatibility interface.
Properties and emitted events should be:
* documented
* typed
* minimal
* backwards compatible where practical
Use namespaced events such as:
```text
barkstack:navigate
barkstack:notify
barkstack:confirm
```
Do not rely on undocumented access to the host application's internal Svelte state.
## 13. Styling
Use the Barkstack design language consistently.
Shared visual values should come from CSS custom properties/design tokens such as:
```text
--bark-bg
--bark-surface
--bark-border
--bark-text
--bark-text-muted
--bark-primary
--bark-danger
--bark-radius-md
```
Avoid hard-coding slightly different versions of the same colours and spacing throughout modules.
Prefer flat, restrained styling consistent with Barkstack's visual identity.
Avoid unnecessary gradients, excessive animation, heavy shadows, or decorative effects that reduce clarity.
Module-specific styling should not leak outside the module.
## 14. CSS
Prefer component-scoped CSS.
Keep selectors simple.
Avoid excessive specificity and `!important`.
Do not use arbitrary global selectors from module code.
Use semantic class names based on purpose rather than visual appearance where practical.
Prefer layout primitives such as:
* flexbox
* grid
* gap
over brittle margin chains and absolute positioning.
## 15. Accessibility
Accessibility is part of correctness.
Use semantic HTML.
Prefer:
```html
<button>
```
over clickable `<div>` elements.
Controls must be usable by keyboard.
Form inputs must have labels.
Icons used as controls require accessible names.
Focus states must remain visible.
Use ARIA only where native HTML semantics are insufficient.
Dialogs must manage focus appropriately.
Colour must not be the only indicator of status.
## 16. Forms
Use semantic form controls.
Validation should occur at useful times and provide actionable messages.
Backend validation remains authoritative.
Do not rely solely on frontend validation for security or correctness.
Avoid clearing user-entered form state after a recoverable submission failure.
Destructive operations should require deliberate user action and appropriate confirmation.
## 17. Tables and Resource Lists
Infrastructure UIs frequently contain tables. Keep them readable and useful.
Resource tables should normally support sensible representations of:
* resource name
* status
* project
* important metadata
* actions
Avoid filling tables with every field available from the API.
Use detail pages for secondary information.
Loading and empty states should not cause major layout shifts.
## 18. Status Representation
Use a consistent vocabulary and visual representation for resource states.
Examples might include:
```text
healthy
degraded
unavailable
stopped
starting
suspended
unknown
```
Do not invent module-specific synonyms where an existing Barkstack status already describes the condition.
Status should never be communicated through colour alone.
## 19. Icons
Use a consistent icon library or Barkstack-provided icon set.
Do not mix several unrelated icon styles.
Icons should support comprehension rather than act as decoration everywhere.
Module manifests may refer to semantic icon names rather than embedding arbitrary HTML or SVG where possible.
## 20. User Feedback
User-triggered operations must produce feedback.
For successful actions, use the shared Barkstack notification mechanism where appropriate.
For failures, show enough information to understand what went wrong.
Long-running operations should indicate progress or ongoing state.
Do not leave buttons appearing active after an operation has completed or failed.
## 21. Confirmation Dialogs
Use confirmation dialogs for genuinely destructive or high-impact actions.
Do not ask for confirmation for ordinary reversible actions.
Confirmation wording should describe the specific consequence:
```text
Delete database "photos"?
This removes the database service. Its volume will be retained.
```
rather than:
```text
Are you sure?
```
Use the Barkstack shell's shared confirmation mechanism rather than implementing different dialogs in each module.
## 22. Browser APIs
Access browser-only globals such as:
```text
window
document
localStorage
navigator
```
only where appropriate.
Keep browser API usage behind clear boundaries so components remain testable.
Do not use local storage as a hidden source of infrastructure configuration.
Local storage may be used for local presentation preferences such as dismissed hints or UI state where appropriate.
## 23. Performance
Do not optimize prematurely.
Avoid unnecessary repeated requests and expensive reactive computations.
Large lists should not trigger complete application rerenders for unrelated state changes.
Lazy-load module UI bundles and expensive views where it provides a meaningful benefit.
Do not introduce complicated caching without a clear invalidation strategy.
## 24. Dependencies
Prefer the platform and Svelte itself before adding third-party frontend dependencies.
Dependencies should solve a real problem rather than save a few lines of straightforward code.
Avoid large UI frameworks that undermine Barkstack's design system or unnecessarily inflate independently shipped module bundles.
Keep module dependency trees reasonably small.
## 25. Testing
Frontend behaviour should be tested at the appropriate level.
Unit/component tests should cover important behaviours such as:
* route selection
* form validation
* loading/error/empty states
* emitted plugin events
* conditional rendering
* resource status display
Prefer testing what the user can observe rather than internal implementation details.
Use browser/end-to-end tests for critical integration flows where component tests cannot provide enough confidence.
Do not mock so much of the system that tests cease to represent real behaviour.
## 26. Validation Before Completion
Before frontend work is considered complete, run the project-configured equivalents of:
```bash
svelte-check
eslint
prettier --check
```
and the frontend test suite.
Run a production build as part of validation.
A change is not complete if it:
* fails TypeScript checking
* produces Svelte compiler warnings that should be fixed
* fails linting
* fails tests
* fails the production build
Do not leave known warnings or failures for later unless they are explicitly documented and accepted.
## 27. Console Errors
The browser console should remain clean during normal usage.
Do not knowingly ship:
* uncaught promise rejections
* failed resource requests
* Svelte warnings
* repeated network failures
* accessibility warnings
Expected backend failures should be handled as application states rather than appearing as unexplained console exceptions.
## 28. Comments
Comments should explain intent, constraints, or non-obvious browser/framework behaviour.
Do not comment obvious markup.
Document unusual workarounds and link them conceptually to the constraint requiring them so future developers know whether they can safely remove them.
## 29. Security
Do not render unsanitized HTML from API responses.
Avoid `{@html}` unless the source and sanitization strategy are explicitly understood.
Do not put secrets, tokens, private keys, or sensitive credentials into:
* frontend bundles
* browser logs
* local storage
* URLs
* DOM attributes
The frontend should assume that anything shipped to the browser is observable by the user.
Authorization must be enforced by the backend even if the UI hides unavailable actions.
## 30. Keep the UI Boring in the Best Way
Barkstack is infrastructure software.
The interface should prioritize:
* clarity
* speed
* density where useful
* consistent interactions
* obvious status
* predictable navigation
over novelty.
Animations and visual flourishes should support understanding rather than distract from it.
A user should be able to understand the state of their homelab quickly without fighting the interface.
## 31. Shared UX Components
Barkstack should favour reusable UX components over bespoke UI implementations.
Common interaction patterns and visual elements should be implemented once in the shared Barkstack UI library and reused across the core console and module UIs.
Examples include:
buttons
form controls
selects
toggles
checkboxes
badges
status indicators
cards
tables
empty states
loading states
error states
dialogs
confirmation prompts
notifications
page headers
breadcrumbs
tabs
code blocks
resource summaries
metric cards
action menus
pagination
search/filter controls
Before implementing a new UI pattern inside a module, first determine whether an equivalent shared component already exists.
If the pattern is likely to be useful elsewhere in Barkstack, prefer adding or extending a shared component rather than implementing a module-specific version.
Module-specific components are appropriate when they represent genuinely domain-specific behaviour, such as a PawSQL database connection panel or a DigDNS zone editor.
Shared components should remain domain-neutral.
Prefer:
Barkstack UI library
├── Button
├── DataTable
├── StatusBadge
├── ConfirmDialog
├── EmptyState
└── ResourceHeader
with modules composing them:
PawSQL
└── DatabaseList
├── ResourceHeader
├── DataTable
└── StatusBadge
rather than each module independently creating its own table, badge, buttons, dialogs and loading states.
Shared components should encapsulate:
consistent styling
accessibility behaviour
keyboard interaction
focus management
validation presentation
loading and disabled states
Barkstack design tokens
common interaction conventions
Do not duplicate shared components merely to make small visual changes.
Extend the shared component API where the requirement is broadly useful.
Do not turn the shared UI library into a collection of module-specific business logic.
The goal is for PawSQL, DigDNS, CollarCA, TreatVault, and future modules to feel like parts of one product rather than independently designed applications.

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@ apps/website/build/
apps/console/server/dist/ apps/console/server/dist/
services/pawsql/internal/adminui/dist/ services/pawsql/internal/adminui/dist/
services/treatvault/internal/adminui/dist/ services/treatvault/internal/adminui/dist/
.DS_Store
sdkscratch/

View File

@@ -11,3 +11,13 @@ Every pull request that changes user-visible behavior **MUST** update the releva
- Use concrete commands and state their assumptions, especially Swarm-manager requirements, bind-mounted files, network exposure, Docker-socket access, authentication, and TLS boundaries. - Use concrete commands and state their assumptions, especially Swarm-manager requirements, bind-mounted files, network exposure, Docker-socket access, authentication, and TLS boundaries.
- Remove or revise documentation for behavior that a pull request replaces. Do not leave deprecated flags, images, or workflows documented. - Remove or revise documentation for behavior that a pull request replaces. Do not leave deprecated flags, images, or workflows documented.
- Run `bun install && bun run build` from `apps/website` after changing the site. The build must continue to emit a fully static `build/` directory. - Run `bun install && bun run build` from `apps/website` after changing the site. The build must continue to emit a fully static `build/` directory.
## Coding Guidelines
Before planning, designing, implementing, reviewing, or modifying code, read the applicable guidelines in `.ai/guidelines`.
All plans, designs, implementations, tests, and code reviews must comply with those guidelines.
Where multiple guidelines apply, follow all of them. If a proposed approach conflicts with a guideline, change the approach rather than ignoring the guideline.
Do not consider work complete until it has been validated according to the applicable guidelines.

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
) )
// PluginAPIVersion is the manifest API version this console server accepts.
const PluginAPIVersion = "barkstack.dev/ui/v1" const PluginAPIVersion = "barkstack.dev/ui/v1"
var ( var (
@@ -15,17 +16,22 @@ var (
elementPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$`) elementPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$`)
) )
// PluginEndpoint is one statically configured plugin backend.
type PluginEndpoint struct { type PluginEndpoint struct {
ID string ID string
BaseURL string BaseURL string
} }
// NavigationItem is one navigation entry a plugin contributes to the
// console sidebar.
type NavigationItem struct { type NavigationItem struct {
Label string `json:"label"` Label string `json:"label"`
Path string `json:"path"` Path string `json:"path"`
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
} }
// PluginManifest is the manifest document a plugin serves at its manifest
// endpoint.
type PluginManifest struct { type PluginManifest struct {
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`
ID string `json:"id"` ID string `json:"id"`
@@ -40,6 +46,8 @@ type PluginManifest struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// ValidateManifest checks a fetched manifest against the Barkfile-configured
// plugin ID and the naming rules for mounts, entries, and elements.
func ValidateManifest(configuredID string, manifest PluginManifest) error { func ValidateManifest(configuredID string, manifest PluginManifest) error {
if manifest.APIVersion != PluginAPIVersion { if manifest.APIVersion != PluginAPIVersion {
return fmt.Errorf("unsupported API version %q", manifest.APIVersion) return fmt.Errorf("unsupported API version %q", manifest.APIVersion)

View File

@@ -29,6 +29,8 @@ type refreshCall struct {
done chan struct{} done chan struct{}
} }
// Registry caches plugin manifests for the configured endpoints and hands
// out validated base URLs and display-ready manifest snapshots.
type Registry struct { type Registry struct {
endpoints map[string]PluginEndpoint endpoints map[string]PluginEndpoint
baseURLs map[string]*url.URL baseURLs map[string]*url.URL
@@ -41,6 +43,8 @@ type Registry struct {
refreshes map[string]*refreshCall refreshes map[string]*refreshCall
} }
// NewRegistry validates the configured endpoints and returns a ready
// Registry with empty manifest state.
func NewRegistry(endpoints []PluginEndpoint, logger *slog.Logger) (*Registry, error) { func NewRegistry(endpoints []PluginEndpoint, logger *slog.Logger) (*Registry, error) {
// A missing logger would silently drop refresh failures; require one. // A missing logger would silently drop refresh failures; require one.
if logger == nil { if logger == nil {
@@ -72,6 +76,9 @@ func NewRegistry(endpoints []PluginEndpoint, logger *slog.Logger) (*Registry, er
return registry, nil return registry, nil
} }
// RefreshIfStale refreshes the cached manifests when they are older than
// maximumAge. Concurrent callers arriving during a refresh wave share its
// result instead of issuing their own requests.
func (r *Registry) RefreshIfStale(ctx context.Context, maximumAge time.Duration) { func (r *Registry) RefreshIfStale(ctx context.Context, maximumAge time.Duration) {
r.mu.Lock() r.mu.Lock()
fresh := time.Since(r.lastRefresh) < maximumAge fresh := time.Since(r.lastRefresh) < maximumAge
@@ -108,6 +115,8 @@ func (r *Registry) RefreshIfStale(ctx context.Context, maximumAge time.Duration)
r.mu.Unlock() r.mu.Unlock()
} }
// Refresh fetches every plugin manifest concurrently and republishes the
// cache. Unreachable plugins keep their most recent valid manifest.
func (r *Registry) Refresh(ctx context.Context) { func (r *Registry) Refresh(ctx context.Context) {
states := make(map[string]pluginState, len(r.endpoints)) states := make(map[string]pluginState, len(r.endpoints))
var mu sync.Mutex var mu sync.Mutex
@@ -156,7 +165,7 @@ func (r *Registry) fetchManifest(ctx context.Context, id string, baseURL *url.UR
if err != nil { if err != nil {
return PluginManifest{}, fmt.Errorf("fetch manifest: %w", err) return PluginManifest{}, fmt.Errorf("fetch manifest: %w", err)
} }
defer response.Body.Close() defer func() { _ = response.Body.Close() }() // best-effort: nothing actionable if the close fails
if response.StatusCode != http.StatusOK { if response.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096)) _, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
return PluginManifest{}, fmt.Errorf("fetch manifest: service returned %s", response.Status) return PluginManifest{}, fmt.Errorf("fetch manifest: service returned %s", response.Status)
@@ -165,16 +174,18 @@ func (r *Registry) fetchManifest(ctx context.Context, id string, baseURL *url.UR
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
var manifest PluginManifest var manifest PluginManifest
if err := decoder.Decode(&manifest); err != nil { if err := decoder.Decode(&manifest); err != nil {
return PluginManifest{}, fmt.Errorf("%w: decode: %v", errInvalidManifest, err) return PluginManifest{}, fmt.Errorf("%w: decode: %w", errInvalidManifest, err)
} }
manifest.Available = false manifest.Available = false
manifest.Error = "" manifest.Error = ""
if err := ValidateManifest(id, manifest); err != nil { if err := ValidateManifest(id, manifest); err != nil {
return PluginManifest{}, fmt.Errorf("%w: %v", errInvalidManifest, err) return PluginManifest{}, fmt.Errorf("%w: %w", errInvalidManifest, err)
} }
return manifest, nil return manifest, nil
} }
// Plugins returns the current manifest snapshot ordered by plugin name, with
// availability and error text filled in for the UI.
func (r *Registry) Plugins() []PluginManifest { func (r *Registry) Plugins() []PluginManifest {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@@ -211,11 +222,12 @@ func invalidManifest(id string) PluginManifest {
return manifest return manifest
} }
// BaseURL returns a copy of the parsed base URL for the configured plugin ID.
func (r *Registry) BaseURL(id string) (*url.URL, bool) { func (r *Registry) BaseURL(id string) (*url.URL, bool) {
baseURL, ok := r.baseURLs[id] baseURL, ok := r.baseURLs[id]
if !ok { if !ok {
return nil, false return nil, false
} }
copy := *baseURL cloned := *baseURL
return &copy, true return &cloned, true
} }