Skip to content

Prepare v0.2.0 release candidate - #42

Merged
UnHeardCoder merged 24 commits into
masterfrom
feature/v0.2.0-ui-overhaul
Jul 4, 2026
Merged

Prepare v0.2.0 release candidate#42
UnHeardCoder merged 24 commits into
masterfrom
feature/v0.2.0-ui-overhaul

Conversation

@UnHeardCoder

@UnHeardCoder UnHeardCoder commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Prepares Cubelit v0.2.0: the full UI overhaul plus a verification campaign that boot-tested every bundled game end-to-end (create → readiness → console probe → config paths → delete).

Features

  • In-app console for every game that supports one, driven by recipe metadata: Source RCON (Minecraft Java, CS2, ARK, ARK ASA, Project Zomboid) and container-exec (Bedrock, Rust, Palworld) transports, with per-game quick commands.
  • Config Files + Managed Files tabs on the generic dashboard, rendered from recipe config_files / file_tabs metadata (typed forms with raw-edit fallback; drag-drop upload).
  • Default install location setting — the create wizard prefills <location>/<server-name>.
  • Recipe seed_files (pre-boot volume seeding with {ENV} substitution) and cap_add (extra container capabilities).
  • Motor Town: Behind The Wheel recipe as Coming Soon — its dedicated server currently requires an owning Steam account + interactive Steam Guard (anonymous steamcmd → No subscription), which doesn't fit one-click hosting yet.
  • Smoke harness now verifies consoles (recipe probe command) and expected paths, and takes --volume-root for placing test volumes on a large disk.

Verification campaign (2026-07-02/03, all green)

Game Ready Console probe
minecraft-java 14s list
minecraft-bedrock 5s list
terraria 70s external
valheim 131s external
fivem 14s external (txAdmin)
palworld 224s Info
project-zomboid 112s players
rust-game ~340s web RCON (no probe)
ark 534s
cs2 2545s status
ark-survival-ascended 781s listplayers

Fixes found by the campaign

  • Readiness watcher survives container restarts during bootstrap (log-stream EOF no longer strands servers in starting).
  • Source RCON follows the Valve spec (empty pre-auth response from CS2/PZ no longer breaks auth or swallows output).
  • delete_server removes root-owned volume files via a one-shot cleanup container (Valheim, PZ).
  • Rust recipe tracks :latest (documented exception — the game force-updates monthly and moves its glibc floor; the frozen full tag died with GLIBC_2.34 not found).
  • CS2 bumped to 4.0.1 (3.0.6 predates CS2's libv8 dependency and segfaults on boot).
  • Corrected readiness patterns for Rust (Server startup complete) and ARK (server is up).
  • ASA no longer advertises Game.ini editing (the image consumes the file every boot).
  • PZ RCON works on first boot via seeded server ini.
  • Bedrock console works via SYS_PTRACE + root exec user.

Validation

  • SQLX_OFFLINE=true cargo check --workspace --all-targets
  • SQLX_OFFLINE=true cargo clippy --workspace --all-targets -- -D warnings
  • SQLX_OFFLINE=true cargo test --workspace (70 tests)
  • bun run check / bun run test (22) / bun run build
  • cd website && bun run check && bun run build

Pre-Merge Requirement

  • Manual desktop UI pass (settings persistence, wizard prefill, console/config tabs, delete).
  • Windows manual acceptance testing (Docker onboarding + WSL2 helpers) before merging and tagging v0.2.0.

Summary by CodeRabbit

  • New Features

    • Added an in-app server console, file editor tabs, and improved dashboard views.
    • Introduced smoke-test support and broader game/server management options.
    • Expanded supported games and added richer artwork, themes, and loading visuals.
  • Bug Fixes

    • Improved onboarding and Docker/Windows handling.
    • Fixed server readiness, cleanup, file mounting, and console reliability issues.
    • Added stronger support for server config editing and file safety checks.
  • Documentation

    • Updated setup, contributing, security, README, and changelog content for the new release.

- New design tokens: deeper dark palette (#0e1012 bg), refined orange accent (#f6851f)
- Dark ↔ light theme toggle persisted to localStorage via getThemeStore()
- Collapsible sidebar: 240px expanded (server list + nav) / 64px icon-only (game icons + status dots)
- SteamGridDB hero art for Minecraft Java + FiveM; oklch gradient fallbacks for all others
- Dashboard: 4-tile stat strip + auto-fill server card grid + dashed "New server" card
- CubelitCard redesign: hero art banner, GameIcon, StatusPill, CPU/mem footer
- Server detail: hero banner for art-having games, inline editable name, tabs via GenericDashboard
- GenericDashboard: Overview (GaugeCards + Sparklines + ConnRow) / Console / Files / Settings tabs
- Create wizard: game picker with GameArt cards, terminal-style creation progress, FiveM sidecar note
- DockerOnboarding redesign: numbered step indicators, WSL2→Docker progress strip
- New components: Cube, StatusPill, GaugeCard, Sparkline, ConnRow, GameArt, GameIcon
- Enable all 8 coming-soon recipes for testing (tags still latest — audit before release)
- Inter + JetBrains Mono via @fontsource (offline-safe)
- Remove StatusRibbon (replaced by StatusPill)
- Bump version to 0.2.0 across all 5 files
- art.ts: SteamGridDB hero/logo/icon for Bedrock, ARK, CS2, Palworld, Project Zomboid, Rust, Valheim + Terraria icon
- theme store: add grid mode (none/dots/lines/cross) with localStorage persistence + cycleGrid()
- app.css: grid background system (data-grid attr on html → .app-main::before pattern)
- app.css: theme color transition (180ms ease on bg/border/color)
- app.css: card-lift hover (translateY -2px + shadow), animate-fade-up/fade-in/scale-in/slide-in-left
- app.css: stagger-1..8 delay classes, skeleton shimmer, sparkline drawPath animation
- app.css: enhanced status dot — running now uses ringPulse (expanding ring) instead of opacity fade
- Sparkline: gradient fill, sparkline-path draw-in animation, unique gradient IDs per instance
- CubelitCard: card-lift + animate-fade-up on mount
- Dashboard: stat strip uses animate-fade-up with stagger
- Sidebar: server list items animate-slide-in-left with stagger
- Create wizard: game cards animate-scale-in with stagger
- Layout: grid toggle button in sidebar (cycles through grid modes)
- Game picker cards: hero height 110→160, showLogo=true, min-width 200→180px
- Settings page: /settings route with theme + grid selectors, About section
- Sidebar: removed theme/grid toggles, added Settings gear nav item
- theme store: add setGrid() for direct mode selection
- Skeleton loaders: SkeletonCard.svelte, orange-tinted skel-shimmer CSS
- Dashboard: shows SkeletonCard x4 while loading, float-cube empty state
- Server detail:  watching page.params.id fixes sidebar nav between servers
- Animations: voxel grid spinner, ping rings, term blocks, breathing cube, bars
- Animations: boot-stages tiles in CreateProgress (PREPARE/PULL/BOOT)
- Animations: btn-shimmer sweep on primary buttons
- Spinner.svelte: replace SVG with voxel grid (sm=28px, md=42px, lg=56px)
- CubelitCard: live getServerStats() for CPU% and memory on running servers
- GenericDashboard: real getPublicIp() instead of hardcoded placeholder
- MinecraftDashboard: GaugeCard + Sparkline + ConnRow replace StatsCards
- system.ts: add getPublicIp() wrapper for get_public_ip Tauri command
- recipes.rs: add optional server_cmd field (Vec<String>, serde default None)
- runner.rs: add server_cmd param to ServerRunner::create_container trait
- containers.rs: pass server_cmd as Docker CMD override in ContainerCreateBody
- local.rs: pass recipe.server_cmd to create_container on server creation
  (update_server_settings recreate passes None — no CMD change on env edits)
- terraria.json: server_cmd = [-autocreate, 2, -worldname, CubelitWorld]
  fixes startup crash (bootstrap.sh requires file OR -autocreate flag)
  also removed WORLD_SIZE env var (controlled by -autocreate arg instead)
- art.ts: Terraria hero uses Steam CDN static JPG (SteamGridDB ones are .webm)
Three issues addressed:

1. create_server emitted "Server is ready!" immediately after spawning a
   readiness watcher, before the log pattern ever matched. The "ready"
   progress event now says "monitoring logs for readiness..." when a watcher
   was spawned, and reserves "Server is ready!" for the no-watcher path.

2. The readiness watcher timeout branch only warned when the container
   crashed, silently treating a timed-out-but-still-running container as
   fully ready. The warn is now unconditional on timeout, with the resolved
   status included, so pattern-never-matched timeouts are visible in logs.

3. Readiness watchers are in-process tokio tasks — on process restart any
   server stuck in "starting" has no watcher to advance it. Added
   reconcile_orphaned_starting_servers, called once at startup after
   sync_all_servers, which promotes "starting" → "running" for containers
   Docker confirms are actually up (with a warning log). Servers with no
   container or a stopped container are left for sync to handle.

Three new unit tests cover the reconcile function's no-container and
non-starting-status no-op cases, and sync_single's preservation of
"starting" when there is no container_id.
Recipes like Valheim (/config + /opt/valheim) and Project Zomboid
(/project-zomboid + /project-zomboid-config) declare multiple volumes,
but only the first was ever bound into the container.

Add `additional_volume_binds(volume_path, recipe)` which, for each
recipe volume beyond the primary (index 0), derives a host path as
`{volume_path}/{last_container_path_segment}` and returns the
Docker bind string. The primary volume path and container_mount_path
stored in the DB are unchanged — backward-compatible for existing servers.

Call sites updated:
- `create_server`: creates host directories for additional volumes then
  includes their binds alongside FiveM's txData extra bind.
- `update_server_settings`: includes additional volume binds when
  recreating the container after a settings change.

FiveM is unaffected (single volume; txData extra bind still generated
separately via `extra_binds_for`).

Four unit tests cover: single-volume no-op, Valheim layout, Project
Zomboid layout, and three-volume recipe.
Introduce CreateGuard to track resources allocated during create_server.
On any failure after the DB row is inserted (image pull, FiveM sidecar
provisioning, main container creation, network connect, container start),
guard.cleanup() removes the main container, sidecar container, Docker
network, DB row, and auto-generated volume directory in that order.
Cleanup errors are logged but never mask the original create error.

Volume cleanup policy: only remove the volume directory when CubeLit
auto-generated the path (config.volume_path was None) AND the directory
did not already exist before the attempt. User-supplied paths and
pre-existing directories are always preserved.

provision_fivem_sidecar now accepts &mut CreateGuard and updates it
incrementally: network_name is set after create_network succeeds,
sidecar_container_id after create_container succeeds. This ensures
cleanup is accurate even if provisioning fails mid-way.

Add cleanup_volume_path() free function and four unit tests covering
the user-provided, pre-existing, and new-path cases, plus DB row
deletion via the guard.
- Button: auto-disable when loading=true, preventing double-submit everywhere
- Create page: disable back button and Create button during server creation
- GenericDashboard: surface applySettings errors in the Apply modal instead of silently swallowing them
- GenericDashboard: keep delete modal open with spinner while delete is in progress; show error if it fails
- GenericDashboard: file delete now requires confirmation via a modal instead of deleting immediately
- MinecraftDashboard: auto-clear backup success message after 5s
- Server detail hero buttons: add disabled:opacity-50 and per-button spinners when actionLoading is true
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@UnHeardCoder, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bed90d14-a63d-46d7-b9dc-2c2eac39467a

📥 Commits

Reviewing files that changed from the base of the PR and between 746c6c9 and b2f7fde.

📒 Files selected for processing (15)
  • CLAUDE.md
  • crates/cli/src/commands/smoke.rs
  • crates/core/src/docker/containers.rs
  • crates/core/src/server/console.rs
  • crates/core/src/server/local.rs
  • crates/core/src/smoke.rs
  • src-tauri/recipes/project-zomboid.json
  • src-tauri/src/commands/file_commands.rs
  • src-tauri/src/commands/system_commands.rs
  • src/lib/components/GameTile.svelte
  • src/lib/components/games/GenericDashboard.svelte
  • src/lib/config/parsers.ts
  • src/routes/+layout.svelte
  • src/routes/create/+page.svelte
  • src/routes/server/[id]/+page.svelte
📝 Walkthrough

Walkthrough

This PR ships Cubelit v0.2.0: a shared Rust console/RCON command transport, a smoke-test harness with a new CLI subcommand, expanded recipe schema (dashboard metadata, readiness patterns, seed files, config fields) applied to 11 bundled recipes, server lifecycle rollback/reconciliation improvements, file read/write and Docker Desktop launch commands, and a full Svelte 5 frontend redesign (theming, dashboards, console UI, settings page), alongside updated docs, version bumps, and website content.

Changes

Core Backend (Rust)

Layer / File(s) Summary
Recipe schema: dashboard/readiness/config/seed files
crates/core/src/recipes.rs
Adds RecipeDashboard, RecipeCommand, RecipeFileTab, RecipeReadiness, RecipeConfigField, RecipeSeedFile types plus validation and contract tests.
Console/RCON command dispatch
crates/core/src/server/console.rs, lifecycle.rs, minecraft.rs, mod.rs, runner.rs
Adds send_server_command/send_rcon_command/exec_command, wires ServerLifecycle, and refactors Minecraft RCON helpers into the shared module.
Docker container creation
crates/core/src/docker/containers.rs
Adds server_cmd/cap_add params, bind deduplication, and remove_host_dir_as_root cleanup.
Readiness watcher rework
crates/core/src/server/watchers.rs
Uses recipe-configured pattern/timeout instead of hardcoded values; adds re-attach logic.
Server lifecycle & rollback
crates/core/src/server/local.rs, types.rs, db/queries.rs
Adds CreateGuard rollback, seed-file writing, additional volume binds, and reconcile_orphaned_starting_servers.
Smoke-test harness
crates/core/src/smoke.rs, lib.rs
Implements run_smoke with polling, console/paths checks, and JSON reporting.

Estimated code review effort: 5 (Critical) | ~120 minutes

CLI, Tauri commands, and recipe data

Layer / File(s) Summary
CLI smoke-test subcommand
crates/cli/src/commands/smoke.rs, main.rs, commands/server.rs
Adds a smoke-test command with reporting output and readiness override support.
Tauri file/console/WSL commands
src-tauri/src/commands/*.rs, lib.rs
Adds read_server_file/write_server_file, send_server_command, reworked WSL diagnostics, and open_docker_desktop.
Bundled recipe definitions
src-tauri/recipes/*.json
Enables/pins tags and adds dashboard/readiness/seed-file metadata for 11 games.

Frontend Application

Layer / File(s) Summary
Theme system & types
src/app.css, src/lib/stores/theme.svelte.ts, settings.svelte.ts, types/*.ts
Introduces CSS-variable theming, persisted theme/settings stores, and expanded recipe/docker types.
UI primitives & game art
src/lib/components/*.svelte, src/lib/games/*.ts
Adds/reworks Button, Card, Modal, GaugeCard, Sparkline, GameArt/GameIcon components.
Config/file management & console
src/lib/config/parsers.ts, ConfigFilesTab.svelte, ManagedFilesTab.svelte, ServerConsole.svelte
Adds structured config editing, file upload/delete, and an interactive server console.
Dashboards & routes
src/lib/components/games/*, src/routes/**
Reworks per-game dashboards, server cards, onboarding, and page layouts (dashboard, create wizard, server detail, settings).

Docs, versioning, and website

Layer / File(s) Summary
Version bumps & release docs
CHANGELOG.md, package.json, */Cargo.toml, tauri.conf.json, README.md, CONTRIBUTING.md, CLAUDE.md, LICENSE, SECURITY.md
Bumps all versions to 0.2.0 and refreshes contributor/security/architecture documentation.
Website content
website/**
Updates env docs, publishes a v0.2.0 audit report, and refreshes download/hero/roadmap/supported-games content.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Frontend
  participant Lifecycle as ServerLifecycle (local.rs)
  participant Console as console.rs
  participant Docker

  Frontend->>Lifecycle: send_server_command(id, command)
  Lifecycle->>Console: send_server_command(id, command)
  alt source_rcon mode
    Console->>Console: send_rcon_command (TCP RCON auth + exchange)
  else docker_exec mode
    Console->>Docker: exec_command (argv in container)
  end
  Console-->>Lifecycle: response text
  Lifecycle-->>Frontend: response text
Loading
sequenceDiagram
  participant CLI as smoke-test CLI
  participant Harness as run_smoke
  participant Host as LocalServerHost
  participant Docker

  CLI->>Harness: run_smoke(SmokeOptions)
  loop per recipe
    Harness->>Host: create_server(recipe)
    Host->>Docker: pull image, create container
    Harness->>Harness: poll_until_ready / console_check / paths_check
  end
  Harness-->>CLI: SmokeReport (JSON + table)
Loading

Possibly related PRs

  • UnHeardCoder/cubelit#13: Added readiness_pattern tests that are directly impacted by this PR's removal/replacement of readiness_pattern in watchers.rs.
  • UnHeardCoder/cubelit#26: Introduced the ServerLifecycle trait that this PR extends with the new send_server_command method.
  • UnHeardCoder/cubelit#33: Both PRs modify create_container in crates/core/src/docker/containers.rs, overlapping at the same function-level code.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the PR’s main purpose: preparing the v0.2.0 release candidate.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/v0.2.0-ui-overhaul

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ycle fixes

- New console transport module (core/server/console.rs): source_rcon and
  docker_exec modes resolved from recipe dashboard metadata; user commands
  always passed as a single argv element
- Source RCON now follows the Valve spec: tolerate the empty pre-auth
  response (CS2, Project Zomboid) and read typed packets with a timeout
- Recipe seed_files: template files written into fresh volumes before first
  boot with {ENV} substitution (fixes PZ first-boot RCON)
- Recipe cap_add wired through to HostConfig (Bedrock needs SYS_PTRACE for
  send-command)
- Readiness watcher re-attaches when the docker log stream EOFs on container
  restart instead of silently dying
- CreateServerConfig.readiness_timeout_override_secs so the smoke harness can
  extend recipe timeouts
- delete_server removes root-owned volume files via a one-shot cleanup
  container (Valheim, PZ)
- Smoke harness: console probe + config/file path verification, volume_root
  option; CLI gains --volume-root and CONSOLE/PATHS report columns
- Rolling-tag exceptions (rust-game, motortown) documented in recipe
  validation
- ServerConsole component for all games with console support; docker_exec
  replies point at the server log, empty replies get an explanatory note
- ConfigFilesTab: recipe config_files rendered as typed forms with raw edit
- ManagedFilesTab: recipe file_tabs (mods/saves/plugins) with drag-drop upload
- GenericDashboard wires the new tabs from recipe dashboard metadata
- Default install location setting (localStorage) — create wizard prefills
  <root>/<server-name> for new servers
- Recipe types extended: cap_add, seed_files, probe, exec_user
…dled games

Every available recipe passed the extended smoke test (create -> ready ->
console probe -> config paths -> delete) on 2026-07-03.

- rust-game: track :latest (game force-updates monthly and moves its glibc
  floor; every frozen tag eventually breaks) and fix readiness pattern to
  'Server startup complete'
- cs2: 3.0.6 -> 4.0.1 (3.0.6 predates CS2's libv8 dependency and segfaults)
- ark: readiness pattern 'server is up' (previous pattern never matched)
- ark-survival-ascended: drop Game.ini config editing — the image renames it
  to .backup on every boot, so edits could not persist
- project-zomboid: seed_files pre-creates the server ini so RCON works on
  first boot
- minecraft-bedrock: cap_add SYS_PTRACE + root exec user for send-command
- console probe commands and dashboard metadata across the lineup
The dedicated server (Steam app 2223650) currently requires an owning Steam
account and interactive Steam Guard entry — anonymous steamcmd returns 'No
subscription' and the community images' baked-in beta password is dead
(Access Denied). Shipped as available:false until a credential-less install
path exists. Recipe, Steam CDN art, and gradient hue are ready to flip.
Date moved to 2026-07-03; added entries for the console/config/file-tab
features, install-location setting, verification-campaign fixes, and the
Motor Town coming-soon recipe.
@UnHeardCoder
UnHeardCoder marked this pull request as ready for review July 3, 2026 18:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (7)
src-tauri/recipes/motortown.json-6-8 (1)

6-8: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Pin default_tag even while available: false.

default_tag is still "latest". The recipe is correctly kept available: false per the PR objective (Steam Guard limitation), but the guideline's "never use latest" rule for default_tag is unconditional and independent of the availability flag — worth pinning now so it's not overlooked when the recipe is later enabled.

As per coding guidelines, "Never use \"latest\" for default_tag; pin a specific Docker tag."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/recipes/motortown.json` around lines 6 - 8, The motortown recipe
still leaves default_tag set to latest even though available is false, and the
tag must be pinned regardless of availability. Update the motortown.json recipe
so the default_tag in the recipe metadata uses a specific Docker tag instead of
latest, keeping the available flag unchanged. Use the motortown recipe entry to
locate the fix.

Source: Coding guidelines

SECURITY.md-41-41 (1)

41-41: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Say bytes, not characters.

The code enforces a 4096-byte limit (value.len()), so the policy should match the actual behavior for UTF-8 values.

♻️ Proposed fix
- Cubelit validates that env var values do not exceed 4096 characters and do not contain null bytes.
+ Cubelit validates that env var values do not exceed 4096 bytes and do not contain null bytes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SECURITY.md` at line 41, The environment variable policy text is inconsistent
with the actual validation in the code. Update the SECURITY.md wording to say
the limit is 4096 bytes, not characters, and align the description with the
behavior enforced by the env var validation logic that uses value length in
bytes and rejects null bytes.
README.md-135-161 (1)

135-161: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tag the structure fence with a language.

markdownlint flags this block as missing a fence language; text or plaintext is enough and keeps the docs lint-clean.

🛠️ Proposed fix
-```
+```text
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 135 - 161, Add a language tag to the fenced structure
block in README.md so markdownlint no longer treats it as an unlabeled fence.
Update the existing directory tree fence to use a plain text language identifier
such as text or plaintext, keeping the content unchanged, and make sure the
fence around the block remains balanced and valid.

Source: Linters/SAST tools

README.md-60-60 (1)

60-60: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Qualify the recipe-only claim.

This only holds for simple/standard games; complex games here still need registry/component/backend work, so “no code changes required” is too broad.

♻️ Proposed fix
-Each game is defined as a JSON recipe in `src-tauri/recipes/`. Adding a new game is as simple as creating a new recipe file — no code changes required.
+Each game is defined as a JSON recipe in `src-tauri/recipes/`. For simple games, adding a new game is as simple as creating a new recipe file — no code changes required.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 60, Qualify the “no code changes required” claim in the
README recipe description so it only applies to simple/standard games; update
the wording in the documented game setup text to reflect that complex games may
still require registry, component, or backend changes. Keep the reference to the
recipe files in src-tauri/recipes/ but remove the overly broad guarantee from
the README copy.
src/lib/components/games/minecraft/MinecraftDashboard.svelte-294-354 (1)

294-354: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Start stats polling when server.status changes

statsInterval is only created during onMount, so opening this dashboard while the server is stopped and then starting it from the same view leaves CPU/memory stuck at . Move the polling setup into a $effect keyed on server.status so it starts and clears with the server state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/games/minecraft/MinecraftDashboard.svelte` around lines
294 - 354, The live stats polling in onMount only runs once, so CPU/memory never
starts updating if server.status changes after the dashboard is opened. Move the
statsInterval setup/cleanup into a $effect that depends on server.status in
MinecraftDashboard.svelte, and keep getServerStats plus the interval clear logic
tied to that effect so polling starts when status becomes running/starting and
stops otherwise.
src/lib/components/CreateProgress.svelte-24-35 (1)

24-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Boot-stage indicator shows no active state during the creating step.

With stageMap = { prepare: 0, pull: 1, boot: 3 }, when step === 'creating' (currentIdx = 2): pull's threshold (1) is already exceeded → done, but boot's threshold (3) isn't reached → todo. No stage renders as active for the entire "creating" phase, since steps includes 'creating' at index 2, distinct from 'starting' (index 3) which boot's threshold targets.

🐛 Suggested fix using stage ranges instead of single-point thresholds
   function stageStatus(stage: 'prepare' | 'pull' | 'boot'): 'done' | 'active' | 'todo' {
-    const stageMap = {
-      prepare: 0,
-      pull:    1,
-      boot:    3,
+    const stageRanges: Record<'prepare' | 'pull' | 'boot', [number, number]> = {
+      prepare: [0, 0],
+      pull:    [1, 1],
+      boot:    [2, 3],
     };
-    const threshold = stageMap[stage];
-    if (currentIdx > threshold) return 'done';
-    if (currentIdx === threshold) return 'active';
+    const [start, end] = stageRanges[stage];
+    if (currentIdx > end) return 'done';
+    if (currentIdx >= start) return 'active';
     return 'todo';
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/CreateProgress.svelte` around lines 24 - 35, The
boot-stage status logic in stageStatus is using single-point thresholds that
skip the creating phase, so no stage becomes active when currentIdx is 2. Update
the CreateProgress.svelte mapping so the prepare, pull, and boot states cover
ranges or include creating explicitly, and adjust stageStatus to mark the
correct stage as active during creating rather than jumping from pull to boot.
Use the existing stageStatus function and the currentIdx/stageMap logic to
locate the fix.
src/lib/components/ConfigFilesTab.svelte-108-116 (1)

108-116: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use lowercase booleans for properties recipes. minecraft-java and minecraft-bedrock both define type: "boolean" fields under server.properties, so this toggle will write True/False there as well; that should be true/false to match the file format.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/ConfigFilesTab.svelte` around lines 108 - 116, The boolean
toggle in ConfigFilesTab.svelte writes capitalized values for recipe-backed
server.properties fields, which should stay lowercase to match the expected file
format. Update the onclick handler for the boolean branch so it stores
true/false instead of True/False, and keep the existing field update flow in
sync for f.key, fields[cf.path], and saved[cf.path].
🧹 Nitpick comments (20)
crates/cli/tests/integration.rs (1)

3-16: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Multiple #[ignore]d smoke tests may run concurrently under default cargo test.

cargo test runs tests in parallel across threads by default. Each smoke_test!-generated test (and smoke_all_recipes) spins up its own LocalServerHost/containers and pulls potentially 15–35 GB images, but the doc comment only shows cargo test -p cubelit-cli -- --ignored without --test-threads=1. This directly contradicts the harness's own design rationale (crates/core/src/smoke.rs's doc comment about avoiding saturating the host by pulling heavy images in parallel), since running several of these tests together would pull multiple huge images concurrently.

Consider documenting --test-threads=1 explicitly in the doc comment, or serializing these tests with a shared mutex/semaphore.

Also applies to: 143-154

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/tests/integration.rs` around lines 3 - 16, The smoke test usage
notes are missing the required single-threaded execution guidance, which can let
multiple ignored tests run in parallel and overwhelm the host. Update the doc
comment in integration.rs to explicitly recommend running these tests with
--test-threads=1 alongside the existing cargo test commands, or otherwise
serialize the smoke_test! and smoke_all_recipes paths with a shared
mutex/semaphore; keep the guidance aligned with the smoke harness rationale in
smoke.rs.
crates/core/src/smoke.rs (2)

194-226: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

parallel option has no effect — always runs sequentially.

opts.parallel only changes the chunk size passed to .chunks(), but the inner loop awaits each recipe one at a time and pushes plain results (not join handles) into handles. Setting parallel > 1 therefore silently does nothing, which is misleading if this is exposed as a CLI flag/config knob.

Either implement real concurrency (e.g., tokio::task::spawn_blocking per recipe, or restructure to avoid the !Send constraint), or simplify the API by dropping/documenting parallel as currently unused so callers aren't misled.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/smoke.rs` around lines 194 - 226, The `parallel` setting in
`smoke.rs` is misleading because the `smoke_one` loop still awaits each recipe
sequentially, so `opts.parallel` only changes chunk size and never creates real
concurrency. Update the `smoke` execution path to either actually run recipes in
parallel using an appropriate task strategy that works with the `host`/`!Send`
constraint, or remove/deprecate the `parallel` option and document that the
current `smoke_one` flow is sequential. Keep the fix centered on the `parallel`,
`handles`, and `smoke_one` logic so the behavior matches the exposed API.

228-246: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Timestamp drift and silent failures in report persistence.

build_report_json is called with a fresh Utc::now() (line 235) that differs from the finished_at used to build the returned SmokeReport (line 244), so the JSON file on disk and the in-memory report disagree on completion time. Additionally, both serde_json::to_string_pretty failure (if let Ok(json) = ...) and std::fs::write failure (let _ = ...) are swallowed without any log line, making persistence failures invisible.

♻️ Proposed fix
+    let finished_at = Utc::now();
     let smoke_dir = host.data_dir.join("smoke");
     if let Err(e) = std::fs::create_dir_all(&smoke_dir) {
         tracing::warn!(error = %e, "Could not create smoke output directory");
     } else {
         let ts = started_at.format("%Y%m%d-%H%M%S");
         let path = smoke_dir.join(format!("cubelit-smoke-{}.json", ts));
-        if let Ok(json) = build_report_json(&results, &started_at, &Utc::now()) {
-            let _ = std::fs::write(&path, json);
-            info!(path = %path.display(), "Smoke report written");
+        match build_report_json(&results, &started_at, &finished_at) {
+            Ok(json) => {
+                if let Err(e) = std::fs::write(&path, json) {
+                    tracing::warn!(error = %e, "Could not write smoke report");
+                } else {
+                    info!(path = %path.display(), "Smoke report written");
+                }
+            }
+            Err(e) => tracing::warn!(error = %e, "Could not serialize smoke report"),
         }
     }
 
     Ok(SmokeReport {
         results,
         started_at,
-        finished_at: Utc::now(),
+        finished_at,
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/smoke.rs` around lines 228 - 246, The smoke report
persistence path has a timestamp mismatch and swallows write/serialization
failures. In `crates/core/src/smoke.rs`, use the same `finished_at` value for
both `build_report_json` and the returned `SmokeReport`, and update the
`build_report_json`/write flow so `serde_json::to_string_pretty` and
`std::fs::write` errors are both logged with `tracing` instead of ignored. Keep
the fix localized around `build_report_json`, `SmokeReport`, and the smoke
output write block so the on-disk JSON and in-memory report stay consistent.
crates/cli/src/commands/smoke.rs (1)

157-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Take &Path instead of &PathBuf for the parameter.

&PathBuf is a common clippy ptr_arg lint target; &Path is more idiomatic and equally usable at all call sites.

♻️ Proposed signature change
-fn write_json_report(report: &cubelit_core::smoke::SmokeReport, path: &PathBuf) {
+fn write_json_report(report: &cubelit_core::smoke::SmokeReport, path: &std::path::Path) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cli/src/commands/smoke.rs` at line 157, The write_json_report helper
is taking an overly specific path type; update its parameter from &PathBuf to
&Path to match idiomatic Rust and avoid the ptr_arg lint. Adjust the
write_json_report function signature and any nearby imports/usages in smoke.rs
so callers can pass path references directly without changing behavior.
website/src/lib/components/DownloadCTA.svelte (1)

48-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

.btn-unavailable now renders a working link — consider a distinct class/label.

The macOS block is now a real, clickable link to GitHub Releases (cursor: pointer, text-decoration: none), but it still uses the btn-unavailable class name and visual treatment (dimmed/disabled-looking), which may read as non-functional to users even though it now navigates away. Consider a dedicated class (e.g. btn-secondary-link) to avoid conflating "disabled" and "external link" states, and to keep the class semantically accurate if other unavailable states reuse this class elsewhere.

Also applies to: 221-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/lib/components/DownloadCTA.svelte` around lines 48 - 55, The
macOS CTA in DownloadCTA.svelte is now a working external link, but it still
uses the semantically misleading btn-unavailable class and disabled-looking
styling. Update the link in the macOS block to use a dedicated class or label
that reflects an active external download link, and keep the existing
unavailable styling reserved for truly non-clickable states. Make sure any
shared styling or reuse of btn-unavailable elsewhere is not affected, and adjust
the related CTA block consistently if needed.
crates/core/src/recipes.rs (1)

411-533: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Extend the bundled-recipe contract test to cover new dashboard/readiness/capability fields.

bundled_recipes_pass_validation validates ids, tags, ports, env vars, and volume paths, but the new schema surface introduced in this PR has no equivalent compile-time guard:

  • dashboard.command.mode is a free-form String; console.rs's send_server_command only special-cases "source_rcon" | "docker_exec" | "external" | "none", so a typo silently falls through to the generic "Unknown console command mode" runtime error instead of failing at test time.
  • readiness.log_pattern is a required String but nothing asserts it's non-empty; an empty pattern would trivially match the first log line.
  • cap_add accepts arbitrary Linux capability strings with no allow-list, so a future recipe could request something like SYS_ADMIN without any guard rail, even though the current documented need is limited to SYS_PTRACE.

Since this test already exists specifically to catch bundled-recipe contract violations at compile time, extending it is cheap and closes an easy gap for future recipe authors.

♻️ Suggested additional invariants
+        const VALID_COMMAND_MODES: &[&str] = &["source_rcon", "docker_exec", "external", "none"];
+        const ALLOWED_CAPS: &[&str] = &["SYS_PTRACE"];
+
         for r in &recipes {
             let ctx = format!("recipe '{}'", r.id);
             ...
+            if let Some(cmd) = r.dashboard.as_ref().and_then(|d| d.command.as_ref()) {
+                assert!(
+                    VALID_COMMAND_MODES.contains(&cmd.mode.as_str()),
+                    "{ctx}: dashboard.command.mode '{}' is not a recognized mode",
+                    cmd.mode
+                );
+            }
+            if let Some(readiness) = &r.readiness {
+                assert!(
+                    !readiness.log_pattern.is_empty(),
+                    "{ctx}: readiness.log_pattern must not be empty"
+                );
+            }
+            for cap in &r.cap_add {
+                assert!(
+                    ALLOWED_CAPS.contains(&cap.as_str()),
+                    "{ctx}: cap_add '{}' is not on the allow-list",
+                    cap
+                );
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/core/src/recipes.rs` around lines 411 - 533, The bundled recipe
validation test needs to cover the new schema fields introduced by this PR.
Extend bundled_recipes_pass_validation to assert dashboard.command.mode is one
of the supported modes used by send_server_command, require
readiness.log_pattern to be non-empty, and restrict cap_add to the approved
capability set currently expected by the recipe contract. Use the existing
per-recipe loop in bundled_recipes_pass_validation and the recipe fields on each
parsed recipe to add these checks alongside the current id/tag/port/env/volume
validations.
src/app.css (2)

103-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Kebab-case keyframe names per Stylelint.

fadeUp, fadeIn, slideInLeft, scaleIn, drawPath, ringPulse violate keyframes-name-pattern. Rename and update all usages (animation: fadeUp ... etc.) accordingly.

🎨 Proposed rename
-@keyframes fadeUp {
+@keyframes fade-up {
   from { opacity: 0; transform: translateY(10px); }
   to   { opacity: 1; transform: translateY(0); }
 }
-@keyframes fadeIn {
+@keyframes fade-in {
   from { opacity: 0; }
   to   { opacity: 1; }
 }
-@keyframes slideInLeft {
+@keyframes slide-in-left {
   from { opacity: 0; transform: translateX(-8px); }
   to   { opacity: 1; transform: translateX(0); }
 }
-@keyframes scaleIn {
+@keyframes scale-in {
   from { opacity: 0; transform: scale(0.96); }
   to   { opacity: 1; transform: scale(1); }
 }
...
-@keyframes drawPath {
+@keyframes draw-path {
   from { stroke-dashoffset: 1; }
   to   { stroke-dashoffset: 0; }
 }
-@keyframes ringPulse {
+@keyframes ring-pulse {
   ...
 }

And update the corresponding .animate-*/.status-dot.running/.sparkline-path rules that reference these names by their old identifiers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.css` around lines 103 - 134, The keyframe names in app.css do not
match the kebab-case pattern enforced by Stylelint. Rename the noncompliant
animations in the `@keyframes` blocks (fadeUp, fadeIn, slideInLeft, scaleIn,
drawPath, ringPulse) to kebab-case, then update every matching animation
reference in the related .animate-* rules, .status-dot.running, and
.sparkline-path so they point to the new identifiers.

Source: Linters/SAST tools


27-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Lowercase font-family fallback keywords per Stylelint.

BlinkMacSystemFont, SFMono-Regular, Menlo trip value-keyword-case.

🎨 Proposed fix
-  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
+  --font-sans: 'Inter', -apple-system, blinkmacsystemfont, 'Segoe UI', sans-serif;
+  --font-mono: 'JetBrains Mono', ui-monospace, sfmono-regular, menlo, monospace;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.css` around lines 27 - 28, The font-family fallback lists in the
stylesheet violate value-keyword-case because some keyword/system font names use
mixed case. Update the font declarations for the CSS variables in app.css to use
lowercase fallback keywords where applicable, including the font stack used by
the `--font-sans` and `--font-mono` definitions, so Stylelint no longer flags
`BlinkMacSystemFont`, `SFMono-Regular`, or `Menlo`.

Source: Linters/SAST tools

src/lib/components/games/GenericDashboard.svelte (1)

52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate port/env parsing instead of shared utilities.

getAddress() and loadEnv() reimplement JSON.parse on port_mappings/environment rather than reusing parsePorts/parseEnv from $lib/utils/server, which FivemDashboard.svelte/MinecraftDashboard.svelte already use. This risks divergent parsing behavior across dashboards.

♻️ Proposed consolidation
-  function getAddress(): string {
-    try {
-      const ports: Record<string, number> = JSON.parse(server.port_mappings);
-      const first = Object.values(ports)[0];
-      if (first) return `localhost:${first}`;
-    } catch { /* ignore */ }
-    return '—';
-  }
+  function getAddress(): string {
+    const ports = parsePorts(server.port_mappings);
+    const first = Object.values(ports)[0];
+    return first ? `localhost:${first}` : '—';
+  }

Also applies to: 102-111

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/games/GenericDashboard.svelte` around lines 52 - 59, The
address/env parsing in GenericDashboard.svelte is duplicated instead of using
the shared server utilities. Update getAddress() and loadEnv() to reuse
parsePorts and parseEnv from $lib/utils/server, matching the approach already
used in FivemDashboard.svelte and MinecraftDashboard.svelte. Keep the existing
behavior the same, but centralize the parsing logic so port_mappings and
environment are handled consistently across dashboards.
src/lib/components/games/minecraft/MinecraftDashboard.svelte (1)

296-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the getPublicIp() API wrapper instead of a manual invoke call.

$lib/api/system.ts already exports getPublicIp() for exactly this purpose; duplicating the raw dynamic invoke("get_public_ip") call here risks the two implementations diverging.

♻️ Proposed fix
+  import { getPublicIp } from '$lib/api/system';
...
   onMount(async () => {
-    // Fetch public IP for the connection card
-    try {
-      const { invoke } = await import("`@tauri-apps/api/core`");
-      publicIp = await invoke<string>("get_public_ip");
-    } catch {
-      publicIp = null;
-    }
+    // Fetch public IP for the connection card
+    publicIp = await getPublicIp().catch(() => null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/games/minecraft/MinecraftDashboard.svelte` around lines
296 - 301, The MinecraftDashboard public IP lookup is duplicating the low-level
Tauri call instead of using the shared API wrapper. Update the logic in
MinecraftDashboard.svelte to call getPublicIp() from $lib/api/system.ts rather
than importing invoke directly, so the component relies on the single existing
wrapper and stays aligned with the rest of the app.
src/lib/components/CubelitCard.svelte (2)

39-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate port-parsing logic instead of shared parsePorts utility.

Same pattern flagged in GenericDashboard.svelte; consolidating on parsePorts from $lib/utils/server avoids divergent behavior across card/dashboard views.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/CubelitCard.svelte` around lines 39 - 46, The address
lookup in getAddress duplicates port-parsing logic that already exists in
parsePorts from $lib/utils/server, so update CubelitCard.svelte to use that
shared utility instead of JSON.parse here. Reuse parsePorts in getAddress and
derive the first available port from its result, keeping the fallback of '—'
when no port is present. Make the same shared-utility approach consistent with
GenericDashboard.svelte so both views resolve server ports the same way.

27-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Card stats fetched once, not refreshed while server keeps running.

The effect only re-fetches when server.status transitions to 'running'; it doesn't poll afterward, so CPU/Memory shown on long-lived server cards go stale. Consider adding a periodic refresh (with cleanup) similar to the dashboard components.

♻️ Proposed fix
   $effect(() => {
     if (server.status !== 'running') {
       cpuPct = null;
       memUsedGb = null;
       return;
     }
-    getServerStats(server.id).then(s => {
-      cpuPct = s.cpu_percent;
-      memUsedGb = s.memory_usage_mb / 1024;
-    }).catch(() => {});
+    const fetchStats = () => getServerStats(server.id).then(s => {
+      cpuPct = s.cpu_percent;
+      memUsedGb = s.memory_usage_mb / 1024;
+    }).catch(() => {});
+    fetchStats();
+    const interval = setInterval(fetchStats, 5000);
+    return () => clearInterval(interval);
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/CubelitCard.svelte` around lines 27 - 37, The
CubelitCard.svelte $effect only fetches stats once when server.status becomes
'running', so the CPU and memory values can go stale on long-lived cards. Update
the CubelitCard effect around getServerStats(server.id) to start a periodic
refresh while the server remains running, and add cleanup to stop the timer when
status changes or the component re-runs; keep the existing null reset path for
non-running servers.
src/lib/components/Input.svelte (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer $props.id() over Math.random() for the fallback id.

Svelte 5.20+ provides $props.id() specifically to generate a component-instance-unique id that stays consistent across SSR/hydration, replacing ad-hoc Math.random() id tricks.

♻️ Proposed fix
-  const uid = $derived(id || `input-${Math.random().toString(36).slice(2, 9)}`);
+  const fallbackId = $props.id();
+  const uid = $derived(id || fallbackId);

Please confirm this app doesn't rely on SSR for this route (if fully client-rendered, the current approach is harmless, but the built-in rune is still the more idiomatic choice).

Also applies to: 26-26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/Input.svelte` at line 6, The fallback id logic in the
Input component should stop using ad-hoc randomness and use Svelte’s built-in
instance-safe id instead. Update the ID generation in Input.svelte to use
$props.id() in the component’s props setup, and make sure the input/label wiring
continues to read from the same unique id symbol used by the component. If this
route is truly client-only, the current approach is not broken, but still
replace it for the idiomatic SSR/hydration-safe behavior.
src/lib/components/Modal.svelte (1)

24-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add dialog semantics for screen readers.

The panel has no role="dialog"/aria-modal, so assistive tech won't announce it as a modal or associate the title with it.

♿ Proposed fix
     <div class="bg-cubelit-surface border border-cubelit-border-2 rounded-2xl max-w-lg w-full mx-4 p-6 shadow-2xl">
+    <div
+      class="bg-cubelit-surface border border-cubelit-border-2 rounded-2xl max-w-lg w-full mx-4 p-6 shadow-2xl"
+      role="dialog"
+      aria-modal="true"
+    >
       {`#if` title}
-        <h2 class="text-sm font-semibold text-cubelit-text mb-4 tracking-tight">{title}</h2>
+        <h2 id="cubelit-modal-title" class="text-sm font-semibold text-cubelit-text mb-4 tracking-tight">{title}</h2>
       {/if}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/Modal.svelte` around lines 24 - 38, The modal container in
Modal.svelte is missing dialog semantics, so assistive technologies won’t treat
it as a modal or connect its title. Update the root element rendered inside the
{`#if` open} block to use dialog semantics with aria-modal and wire the optional
title heading to the dialog via an accessible label relationship; use the
existing title render block and handleBackdrop/children structure as the
reference points for the change.
src/routes/+layout.svelte (1)

167-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a shared nav-item snippet.

The Dashboard/New server/Settings buttons repeat the same conditional class logic three times, differing only in icon, label, and route.

♻️ Sketch using a Svelte 5 snippet
{`#snippet` navItem(href, label, active, path)}
  <button type="button" onclick={() => goto(href)} title={iconMode ? label : undefined}
    class="flex items-center gap-2.5 rounded-lg transition-colors text-[13px] font-medium
      {iconMode ? 'justify-center p-2.5' : 'px-2.5 py-2'}
      {active ? 'bg-cubelit-accent/15 text-cubelit-accent' : 'text-cubelit-text-dim hover:text-cubelit-text hover:bg-cubelit-surface'}">
    {`@render` path()}
    {`#if` !iconMode}<span>{label}</span>{/if}
  </button>
{/snippet}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/`+layout.svelte around lines 167 - 219, The nav buttons in
+layout.svelte repeat the same button/class/title pattern for Dashboard, New
server, and Settings, so extract that shared logic into a reusable Svelte
snippet or helper. Update the nav block around the existing goto calls and
isDashboard/isCreatePage/page.url.pathname checks to render via a single navItem
snippet that accepts href, label, active state, and icon content, while
preserving the current iconMode behavior and active styling.
src/lib/components/Sparkline.svelte (1)

37-40: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Gradient ID can collide across instances.

The gradient id spark-{hue}-{seed} is not guaranteed unique per component instance—only unique per hue/seed combination. If two Sparklines with the same hue+seed ever render simultaneously (e.g., a future multi-server overview), one instance's gradient definition will silently override the other's.

Consider deriving a per-instance id (e.g., Svelte 5's $props.id()) instead of hue/seed.

♻️ Suggested fix
-  let { base = 0, hue = 30, seed = 1, height = 80 }: Props = $props();
+  let { base = 0, hue = 30, seed = 1, height = 80 }: Props = $props();
+  const uid = $props.id();
-    <linearGradient id="spark-{hue}-{seed}" x1="0" y1="0" x2="0" y2="1">
+    <linearGradient id="spark-{uid}" x1="0" y1="0" x2="0" y2="1">
-  <path d={fillPath} fill="url(`#spark-`{hue}-{seed})" />
+  <path d={fillPath} fill="url(`#spark-`{uid})" />

Note: $props.id() was added in Svelte 5.20.0; please confirm the project's pinned Svelte version supports it before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/Sparkline.svelte` around lines 37 - 40, The gradient
identifier in Sparkline is only based on hue/seed, so it can collide between
simultaneous component instances. Update the Sparkline component to use a
per-instance unique id for the <linearGradient> and matching fill/reference,
preferably via Svelte 5’s $props.id() if the project’s pinned Svelte version
supports it. Keep the existing hue-based color values, but replace the current
spark-{hue}-{seed} naming so each Sparkline instance owns its own gradient
definition.
src/lib/components/ServerConsole.svelte (1)

7-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prop interface should be named ServerConsoleProps.

As per coding guidelines, Svelte components should type props with interface XxxProps { ... }; this uses a plain interface Props.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/ServerConsole.svelte` around lines 7 - 13, Rename the
Svelte props interface in ServerConsole to ServerConsoleProps and update the
destructuring type annotation to use that name; keep the existing server and
command fields unchanged, and make sure the component’s prop typing consistently
references ServerConsoleProps wherever the props are declared.

Source: Coding guidelines

src/lib/components/ConfigFilesTab.svelte (2)

8-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prop interface should be named ConfigFilesTabProps.

As per coding guidelines, Svelte components should type props with interface XxxProps { ... }; this uses a plain interface Props.

♻️ Suggested rename
-  interface Props {
+  interface ConfigFilesTabProps {
     server: Cubelit;
     configFiles: RecipeConfigFile[];
   }

-  let { server, configFiles }: Props = $props();
+  let { server, configFiles }: ConfigFilesTabProps = $props();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/ConfigFilesTab.svelte` around lines 8 - 13, The props
interface in ConfigFilesTab.svelte is using the generic name Props instead of
the required component-specific naming convention. Rename the interface to
ConfigFilesTabProps and update the $props() type annotation in the
ConfigFilesTab component to use that name so the prop typing matches the coding
guidelines.

Source: Coding guidelines


118-126: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded hex colors bypass the theme system.

The <option> style uses raw hex (#23272f/#f5f5f6) instead of the cubelit-* theme tokens used elsewhere in this component.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/ConfigFilesTab.svelte` around lines 118 - 126, The
<select>/<option> styling in ConfigFilesTab.svelte is bypassing the theme system
by using hardcoded hex colors. Update the <option> style inside the
ConfigFilesTab component to use the existing cubelit-* theme tokens consistent
with the rest of the component, and keep the change localized around the select
rendering that uses fields, saved, and f.options.
src/lib/components/ManagedFilesTab.svelte (1)

11-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prop interface should be named ManagedFilesTabProps.

As per coding guidelines, Svelte components should type props with interface XxxProps { ... }; this uses a plain interface Props.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/components/ManagedFilesTab.svelte` around lines 11 - 16, The prop
typing in ManagedFilesTab should follow the project’s naming convention instead
of using a generic Props interface. Rename the interface used by the $props()
destructuring in ManagedFilesTab.svelte to ManagedFilesTabProps, and update the
typed destructuring to reference that new interface so the component’s prop type
name is explicit and consistent with the rest of the codebase.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfcab433-7fcc-485c-a7ca-461ab1b05768

📥 Commits

Reviewing files that changed from the base of the PR and between ffab5fb and 746c6c9.

⛔ Files ignored due to path filters (52)
  • Cargo.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
  • src-tauri/icons/128x128.png is excluded by !**/*.png
  • src-tauri/icons/128x128@2x.png is excluded by !**/*.png
  • src-tauri/icons/32x32.png is excluded by !**/*.png
  • src-tauri/icons/64x64.png is excluded by !**/*.png
  • src-tauri/icons/Square107x107Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square142x142Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square150x150Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square284x284Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square30x30Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square310x310Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square44x44Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square71x71Logo.png is excluded by !**/*.png
  • src-tauri/icons/Square89x89Logo.png is excluded by !**/*.png
  • src-tauri/icons/StoreLogo.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png is excluded by !**/*.png
  • src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • src-tauri/icons/icon.ico is excluded by !**/*.ico
  • src-tauri/icons/icon.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-20x20@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-29x29@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@2x-1.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-40x40@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-512@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-60x60@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-60x60@3x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-76x76@1x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-76x76@2x.png is excluded by !**/*.png
  • src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png is excluded by !**/*.png
  • static/favicon.png is excluded by !**/*.png
📒 Files selected for processing (101)
  • .cursor/.gitignore
  • CHANGELOG.md
  • CLAUDE.md
  • CONTRIBUTING.md
  • LICENSE
  • README.md
  • SECURITY.md
  • crates/cli/Cargo.toml
  • crates/cli/src/commands/mod.rs
  • crates/cli/src/commands/server.rs
  • crates/cli/src/commands/smoke.rs
  • crates/cli/src/context.rs
  • crates/cli/src/main.rs
  • crates/cli/src/resolve.rs
  • crates/cli/src/sink.rs
  • crates/cli/tests/integration.rs
  • crates/core/Cargo.toml
  • crates/core/src/db/queries.rs
  • crates/core/src/docker/containers.rs
  • crates/core/src/lib.rs
  • crates/core/src/recipes.rs
  • crates/core/src/server/console.rs
  • crates/core/src/server/lifecycle.rs
  • crates/core/src/server/local.rs
  • crates/core/src/server/minecraft.rs
  • crates/core/src/server/mod.rs
  • crates/core/src/server/runner.rs
  • crates/core/src/server/types.rs
  • crates/core/src/server/watchers.rs
  • crates/core/src/smoke.rs
  • package.json
  • src-tauri/Cargo.toml
  • src-tauri/icons/android/values/ic_launcher_background.xml
  • src-tauri/icons/icon.icns
  • src-tauri/recipes/ark-survival-ascended.json
  • src-tauri/recipes/ark.json
  • src-tauri/recipes/cs2.json
  • src-tauri/recipes/fivem.json
  • src-tauri/recipes/minecraft-bedrock.json
  • src-tauri/recipes/minecraft-java.json
  • src-tauri/recipes/motortown.json
  • src-tauri/recipes/palworld.json
  • src-tauri/recipes/project-zomboid.json
  • src-tauri/recipes/rust-game.json
  • src-tauri/recipes/terraria.json
  • src-tauri/recipes/valheim.json
  • src-tauri/src/commands/file_commands.rs
  • src-tauri/src/commands/minecraft_commands.rs
  • src-tauri/src/commands/system_commands.rs
  • src-tauri/src/lib.rs
  • src-tauri/tauri.conf.json
  • src/app.css
  • src/lib/api/files.ts
  • src/lib/api/minecraft.ts
  • src/lib/api/system.ts
  • src/lib/components/Button.svelte
  • src/lib/components/Card.svelte
  • src/lib/components/ConfigFilesTab.svelte
  • src/lib/components/ConnRow.svelte
  • src/lib/components/CreateProgress.svelte
  • src/lib/components/Cube.svelte
  • src/lib/components/CubelitCard.svelte
  • src/lib/components/DockerOnboarding.svelte
  • src/lib/components/GameArt.svelte
  • src/lib/components/GameIcon.svelte
  • src/lib/components/GameTile.svelte
  • src/lib/components/GaugeCard.svelte
  • src/lib/components/Input.svelte
  • src/lib/components/ManagedFilesTab.svelte
  • src/lib/components/Modal.svelte
  • src/lib/components/ServerConsole.svelte
  • src/lib/components/SkeletonCard.svelte
  • src/lib/components/Sparkline.svelte
  • src/lib/components/Spinner.svelte
  • src/lib/components/StatusPill.svelte
  • src/lib/components/StatusRibbon.svelte
  • src/lib/components/games/GenericDashboard.svelte
  • src/lib/components/games/fivem/FivemDashboard.svelte
  • src/lib/components/games/minecraft/MinecraftDashboard.svelte
  • src/lib/config/parsers.test.ts
  • src/lib/config/parsers.ts
  • src/lib/games/art.ts
  • src/lib/games/registry.ts
  • src/lib/stores/settings.svelte.ts
  • src/lib/stores/theme.svelte.ts
  • src/lib/types/docker.ts
  • src/lib/types/recipe.ts
  • src/routes/+layout.svelte
  • src/routes/+page.svelte
  • src/routes/create/+page.svelte
  • src/routes/server/[id]/+page.svelte
  • src/routes/settings/+page.svelte
  • website/.env.example
  • website/README.md
  • website/package.json
  • website/public/audits/manifest.json
  • website/public/audits/v0.2.0.html
  • website/src/lib/components/DownloadCTA.svelte
  • website/src/lib/components/Hero.svelte
  • website/src/lib/components/Roadmap.svelte
  • website/src/lib/components/SupportedGames.svelte
💤 Files with no reviewable changes (1)
  • src/lib/components/StatusRibbon.svelte

Comment thread crates/cli/src/commands/smoke.rs
Comment thread crates/core/src/docker/containers.rs
Comment thread crates/core/src/server/console.rs
Comment thread crates/core/src/server/local.rs Outdated
Comment thread crates/core/src/server/local.rs
Comment thread src/lib/types/recipe.ts
Comment on lines +93 to +100
export type ConfigFormat = "properties" | "ini" | "cfg" | "lua" | "json" | "text";

export interface RecipeConfigFile {
path: string;
format: ConfigFormat | string;
label: string;
fields?: RecipeConfigField[];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

ConfigFormat is duplicated across files.

This union is defined verbatim again in src/lib/config/parsers.ts (line 15). Two independent declarations of the same literal union will silently drift if one is updated without the other (e.g., adding a new format). Consider importing ConfigFormat from one canonical location (parsers.ts, since it owns the parsing logic that actually depends on the exact set of formats) instead of redeclaring it here.

Separately, format: ConfigFormat | string collapses to string in TypeScript (a union with string absorbs all literal members), so it provides no more type safety/autocomplete than string alone.

♻️ Proposed fix
-export type ConfigFormat = "properties" | "ini" | "cfg" | "lua" | "json" | "text";
+import type { ConfigFormat } from "$lib/config/parsers";

 export interface RecipeConfigFile {
   path: string;
-  format: ConfigFormat | string;
+  format: ConfigFormat;
   label: string;
   fields?: RecipeConfigField[];
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type ConfigFormat = "properties" | "ini" | "cfg" | "lua" | "json" | "text";
export interface RecipeConfigFile {
path: string;
format: ConfigFormat | string;
label: string;
fields?: RecipeConfigField[];
}
import type { ConfigFormat } from "$lib/config/parsers";
export interface RecipeConfigFile {
path: string;
format: ConfigFormat;
label: string;
fields?: RecipeConfigField[];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/types/recipe.ts` around lines 93 - 100, ConfigFormat is duplicated
and should be sourced from one canonical definition instead of being redeclared
in RecipeConfigFile. Update the recipe types in recipe.ts to import ConfigFormat
from parsers.ts (the place that owns the supported format set), so both
locations stay in sync. While there, simplify RecipeConfigFile.format because
ConfigFormat | string is just string in TypeScript; use the shared ConfigFormat
type (or plain string if broader input is intended) so the field has meaningful
typing and autocomplete.

Comment thread src/routes/+layout.svelte
Comment thread src/routes/create/+page.svelte
Comment thread src/routes/server/[id]/+page.svelte
Comment thread src/routes/server/[id]/+page.svelte Outdated
Rust:
- Guard the root cleanup container against suspicious volume paths
  (absolute, ..-free, min depth, never the home directory) with tests
- exec_command gets a 30s timeout like the RCON path
- update_server_settings loads the recipe before the destructive
  stop/remove so a missing recipe can't recreate a container without
  its server_cmd/cap_add/binds contract
- write_seed_files rejects backslash separators and drive prefixes
- Smoke poll distinguishes Ready (readiness pattern matched) from
  Started (no pattern; container running) and drops dead arms
- CLI truncate() cuts at char boundaries (no panic on multi-byte UTF-8)
- copy/write file commands verify the nearest existing ancestor before
  create_dir_all and refuse symlinked destinations
- A single failed DISM probe now reports CheckFailed instead of
  misclassifying the unknown feature state as NeedsInstall (+ test)

Frontend:
- Updater banner recovers from a failed download/install
- Rename input: Escape reliably discards (blur-save guarded), cancel
  restores the original name
- Server page load effect is cancellable — stale loads can't overwrite
  state or leak status listeners on id change
- Stats polling keyed on server.status via $effect (starts when a
  server transitions to running after mount)
- GameTile shows recipe tags instead of a fabricated itzg/* image name
- ConfigFormat single-sourced from types/recipe.ts
- Create wizard: user-edited install paths are passed through verbatim;
  hand-typed ~ paths are rejected with a clear message

Recipes/docs:
- PZ seeds RCONPassword={RCON_PASSWORD} directly
- CLAUDE.md documents the ROLLING_TAG_EXCEPTIONS mechanism
@UnHeardCoder
UnHeardCoder merged commit 3fd4ae1 into master Jul 4, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant