This document is the entry point for AI agents (Claude Code, OpenCode, etc.) working in this repo. It captures everything needed to understand the codebase, make a focused change, validate it, and open a PR without re-reading the whole tree.
RackPeek is a CLI + Web UI for documenting and managing home-lab / small-scale IT infrastructure (servers, switches, routers, firewalls, access points, UPS units, desktops, laptops, systems, and services).
config/config.yaml) — no database.rpk) and the Blazor Server Web UI.aptacode/rackpeek) and a self-contained CLI binary.If a proposed change conflicts with these values, push back before implementing.
| Layer | Tech |
|---|---|
| Language | C# (.NET 10.0, net10.0 TFM) |
| CLI | Spectre.Console.Cli |
| Web UI | Blazor Server (Microsoft.NET.Sdk.Web) + a WASM viewer (RackPeek.Web.Viewer) for the live demo |
| Persistence | YAML (YamlDotNet, DocMigrator.Yaml) — single file |
| Git integration | LibGit2Sharp (optional, used when GIT_TOKEN is set) |
| CLI tests | xUnit + Spectre.Console.Testing + JsonSchema.Net |
| E2E tests | xUnit + Microsoft.Playwright + Testcontainers (spins up the real Docker image) |
| Build runner | just |
| Container | mcr.microsoft.com/dotnet/aspnet:10.0 — exposes port 8080 |
| Code style | dotnet format (CI gate) + .editorconfig |
| Analysis | TreatWarningsAsErrors=true, EnforceCodeStyleInBuild=true, latest analyzers (see Directory.Build.props) |
.NET 10 is required. If dotnet --version shows < 10, see docs/development/dev-setup.md. A devcontainer is included (.devcontainer/).
RackPeek.sln
├── RackPeek/ CLI entry point (Spectre.Console.Cli) → produces `rpk`
├── RackPeek.Domain/ Core domain: resources, use-cases, persistence, git
│ ├── Resources/ Resource models (Server, Switch, System, Service, …)
│ ├── UseCases/ Generic use-cases (Add, Delete, Rename, Cpus, Drives, Gpus, Ports, Labels, Tags, Ansible, SSH, Hosts)
│ ├── Persistence/ IResourceCollection, Yaml repositories, migrations
│ │ └── Yaml/ YamlResourceCollection, RackPeekConfigMigrationDeserializer, ResourceYamlMigrationService
│ ├── Git/ Optional LibGit2Sharp integration (NullGitRepository when disabled)
│ ├── Api/ InventoryRequest/Response + UpsertInventoryUseCase (used by Web API)
│ └── ServiceCollectionExtensions.cs DI: AddUseCases / AddYamlRepos / AddGitServices
├── Shared.Rcl/ Razor Class Library: Blazor components AND CLI command wiring shared between Web + CLI
│ ├── Commands/ Spectre.Console.Cli command classes (one folder per resource kind)
│ ├── Components/ Shared Razor components
│ ├── Modals/, Layout/, Services/, Console/
│ ├── CliBootstrap.cs Registers all CLI commands + DI internals (single source of truth for the `rpk` command tree)
│ └── ConsoleRunner.cs Lets the Web UI execute CLI commands in-process
├── RackPeek.Web/ Blazor Server host (Dockerfile lives here)
├── RackPeek.Web.Viewer/ Blazor WebAssembly viewer (powers the github-pages demo)
├── Tests/ CLI integration tests (xUnit) — fast, no Docker
│ ├── EndToEnd/ Per-resource workflow tests using the real CLI
│ ├── Api/ Web API endpoint tests (Microsoft.AspNetCore.Mvc.Testing)
│ ├── TestConfigs/v1,v2,v3/ Fixture YAML files for migration tests
│ └── schemas/ JSON schemas validated against output
└── Tests.E2e/ Playwright + Testcontainers — spins up the Docker image and drives the Web UI
├── PageObjectModels/ One POM per page/component (required pattern)
└── Infra/PlaywrightFixture.cs Container + browser lifecycle
| You're adding… | Goes in… |
|---|---|
| A new CLI subcommand | Shared.Rcl/Commands/<ResourceKind>/… + register it in Shared.Rcl/CliBootstrap.cs |
| A new resource kind | RackPeek.Domain/Resources/<Kind>/ model, register in Resource.cs maps, add YAML migration, wire repos in ServiceCollectionExtensions.cs, add Razor pages under Shared.Rcl/<Kind>/, add Web routing |
| A new use-case | RackPeek.Domain/UseCases/ — implement IUseCase (auto-registered by reflection in AddUseCases) or the generic IResourceUseCase<T> |
| A new Razor component used by CLI+Web | Shared.Rcl/Components/ |
| A new Web page only | RackPeek.Web/Components/ |
| A YAML schema change | Bump schema version under schemas/vN/ + add migration in RackPeek.Domain/Persistence/Yaml/ + add migration test in Tests/TestConfigs/vN/ |
All workflow commands go through justfile. Prefer just <target> over running dotnet directly so behaviour stays consistent with CI.
just build # dotnet build RackPeek.sln (Debug)
just build-release # Release
just build-cli # publish self-contained single-file binary (default linux-x64)
just build-cli linux-arm64 # cross-target
just build-web # docker build -t rackpeek:ci -f RackPeek.Web/Dockerfile .
just test-cli # fast CLI tests, no Docker required
just e2e-setup # ONCE: installs Playwright CLI + browsers
just test-e2e # implies build-web; runs Playwright suite
just test-all # = build-web + e2e-setup + test-cli + test-e2e
just ci # alias for test-all — matches the pre-PR checklist
CI order (.github/workflows/test.yml):
format → dotnet format --verify-no-changes (runs on ubuntu-latest)cli-tests → dotnet test Tests (runs on ubuntu-latest, depends on format)webui-tests → builds the docker image then runs dotnet test Tests.E2e (runs on ubuntu-24.04, depends on cli-tests)Always run dotnet format before commit — formatting breaks CI first.
just run-docker # build + run container on http://localhost:8080
just rpk [args] # run CLI directly from Debug build
just clean # dotnet clean
just docker-push 1.3.2 # multi-arch (linux/amd64, linux/arm64) push to aptacode/rackpeek
CLI binary version is bumped in RackPeek/RackPeek.csproj (<AssemblyVersion>).
just build-cli-demo # VHS recording — needs vhs, imagemagick, chrome
just build-web-demo # GIF capture — needs Chrome, ImageMagick
Enforced by CI via dotnet format --verify-no-changes. From .editorconfig + Directory.Build.props:
var for built-in types and when the type is apparent; explicit type otherwise._camelCase (underscore prefix, error severity).csharp_new_line_before_open_brace = all:error.<Nullable>enable</Nullable>).Default to writing no comments. The project favours readable names + tests-as-documentation.
There is one YAML file: config/config.yaml (or the path given by RPK_YAML_DIR env var; the Docker image sets it to /app/config).
Top-level shape:
resources:
- kind: Server | Switch | Firewall | Router | Accesspoint | Desktop | Laptop | Ups | System | Service
name: <unique name within kind>
tags: [...]
labels: { key: value }
notes: |
free-form markdown
runsOn: [<parent-resource-name>, ...] # only meaningful for System / Service
# kind-specific fields follow (e.g. ports[], cpus[], drives[], gpus[], nics[], network, ram, …)
Key invariants (see RackPeek.Domain/Resources/Resource.cs):
name is the identity within a kind. Don't introduce numeric IDs.runsOn relationships are validated by Resource.CanRunOn<T>:
Service may run on a System.System may run on hardware (Server, Switch, Firewall, Router, Accesspoint, Desktop, Laptop, Ups) or on another System.Resource.IsHardware).IResourceUseCase<T> → IResourceCollection → repository, never direct file writes.Schemas are versioned under schemas/v1, schemas/v2, schemas/v3. Migration code lives in RackPeek.Domain/Persistence/Yaml/:
RackPeekConfigMigrationDeserializer.cs — deserialisation entry pointResourceYamlMigrationService.cs — applies the version chainWhen you change persisted YAML shape, the PR must include:
schemas/vN+1/schema.vN+1.json.Tests/TestConfigs/vN+1/ (note the explicit <None Update> entries in Tests/Tests.csproj if you add new files).The full command tree is documented in docs/Commands.md and docs/CommandIndex.md (auto-generated by generate-docs.sh). At a glance:
rpk <kind> <verb> [name] [flags]
kinds: summary, servers, switches, routers, firewalls, systems,
accesspoints, ups, desktops, laptops, services
verbs: summary, add, list, get, describe, set, del, tree
sub: cpu, drive, gpu, nic, port, subnets, labels, tags, rename, …
When adding/altering commands, regenerate the docs (./generate-docs.sh) so the published reference stays in sync.
| Var | Default | Purpose |
|---|---|---|
RPK_YAML_DIR |
config (CLI) / /app/config (Docker) |
Directory containing config.yaml |
GIT_TOKEN |
unset | If set, enables LibGit2GitRepository for the config dir |
GIT_USERNAME |
git |
Username paired with GIT_TOKEN |
ASPNETCORE_URLS |
http://+:8080 (Docker) |
Web UI bind |
Read docs/development/testing-guidelines.md in full before touching tests. Highlights:
Tests/) drive the real CommandApp, assert exact stdout, and inspect the YAML written to disk. Use the ExecuteAsync(...) helper pattern.Tests.E2e/) use Testcontainers to run the real Docker image then drive the Web UI via Playwright. Every page has a Page Object Model (POM) in Tests.E2e/PageObjectModels/. Tests should read like workflows, not browser scripts.Guid.NewGuid() and delete what you create.dotnet format cleanjust ci green locallyFrom docs/development/contribution-guidelines.md:
just ci)Headless = false in PlaywrightFixture.cs)Default branches: feature work targets staging; releases flow staging → main.
just test-e2e rebuilds it via just build-web. If you change anything in RackPeek.Web, RackPeek.Domain, or Shared.Rcl, the image must be rebuilt before E2E runs.just e2e-setup. In CI they're cached under ~/.cache/ms-playwright.Microsoft.Playwright package invalidates the browser cache. Each Playwright version pins a specific Chromium build (e.g. 1.58 → chromium_headless_shell-1208, 1.59 → -1217). After bumping, every E2E test fails fast with PlaywrightException : Executable doesn't exist at .../chromium_headless_shell-NNNN. Re-run just e2e-setup (or ~/.dotnet/tools/playwright install chromium) to download the matching build before running the suite.rackpeek:ci locally (referenced by Tests.E2e/Infra/PlaywrightFixture.cs:9); the registry tag is aptacode/rackpeek.Headless = false, SlowMo = 1500 in Tests.E2e/Infra/PlaywrightFixture.cs. Always revert before commit — CI requires headless.unused-variable warning fails the whole build. Don't add #pragma warning disable to push through; fix the warning.GIT_TOKEN is absent (NullGitRepository). Don't assume git is wired up.RackPeek.Web/config copy/ directory looks like cruft but is checked-in — leave it alone unless cleaning up is the explicit goal.rpk is placed in /usr/local/bin). You can docker exec rackpeek rpk ... against a running container.| Path | What |
|---|---|
justfile |
Single source of truth for developer commands |
RackPeek.sln |
Solution root |
Directory.Build.props |
Repo-wide MSBuild props (analyzers, warnings-as-errors) |
.editorconfig |
Formatting + naming rules |
.github/workflows/test.yml |
CI pipeline (format → cli-tests → webui-tests) |
.github/workflows/publish-*.yml |
Release pipelines |
RackPeek.Web/Dockerfile |
Multi-stage build for the runtime image |
RackPeek/Program.cs |
CLI entry point |
RackPeek.Web/Program.cs |
Web entry point + DI wiring |
Shared.Rcl/CliBootstrap.cs |
Master CLI command registration |
RackPeek.Domain/ServiceCollectionExtensions.cs |
Domain DI registration |
RackPeek.Domain/Resources/Resource.cs |
Resource base + kind/relationship rules |
docs/development/contribution-guidelines.md |
PR process |
docs/development/dev-cheat-sheet.md |
Build / release / Docker / Playwright details |
docs/development/dev-setup.md |
First-time environment setup |
docs/development/testing-guidelines.md |
Testing philosophy + examples |
docs/Commands.md / docs/CommandIndex.md |
Auto-generated CLI reference |
schemas/v1,v2,v3/ |
Versioned YAML schemas |
README.md |
User-facing overview, Docker install, links |
LICENSE |
License terms |