Prepare v0.2.0 release candidate - #42
Conversation
- 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
|
Warning Review limit reached
Next review available in: 23 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThis 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. ChangesCore Backend (Rust)
Estimated code review effort: 5 (Critical) | ~120 minutes CLI, Tauri commands, and recipe data
Frontend Application
Docs, versioning, and website
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
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)
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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.
There was a problem hiding this comment.
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 winPin
default_tageven whileavailable: false.
default_tagis still"latest". The recipe is correctly keptavailable: falseper the PR objective (Steam Guard limitation), but the guideline's "never uselatest" rule fordefault_tagis 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\"fordefault_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 winSay 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 winTag the structure fence with a language.
markdownlintflags this block as missing a fence language;textorplaintextis 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 winQualify 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 winStart stats polling when
server.statuschanges
statsIntervalis only created duringonMount, 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$effectkeyed onserver.statusso 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 winBoot-stage indicator shows no active state during the
creatingstep.With
stageMap = { prepare: 0, pull: 1, boot: 3 }, whenstep === 'creating'(currentIdx = 2): pull's threshold (1) is already exceeded →done, but boot's threshold (3) isn't reached →todo. No stage renders asactivefor the entire "creating" phase, sincestepsincludes'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 winUse lowercase booleans for properties recipes.
minecraft-javaandminecraft-bedrockboth definetype: "boolean"fields underserver.properties, so this toggle will writeTrue/Falsethere as well; that should betrue/falseto 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 winMultiple
#[ignore]d smoke tests may run concurrently under defaultcargo test.
cargo testruns tests in parallel across threads by default. Eachsmoke_test!-generated test (andsmoke_all_recipes) spins up its ownLocalServerHost/containers and pulls potentially 15–35 GB images, but the doc comment only showscargo test -p cubelit-cli -- --ignoredwithout--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=1explicitly 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
paralleloption has no effect — always runs sequentially.
opts.parallelonly changes the chunk size passed to.chunks(), but the inner loopawaits each recipe one at a time and pushes plain results (not join handles) intohandles. Settingparallel > 1therefore 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_blockingper recipe, or restructure to avoid the!Sendconstraint), or simplify the API by dropping/documentingparallelas 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 winTimestamp drift and silent failures in report persistence.
build_report_jsonis called with a freshUtc::now()(line 235) that differs from thefinished_atused to build the returnedSmokeReport(line 244), so the JSON file on disk and the in-memory report disagree on completion time. Additionally, bothserde_json::to_string_prettyfailure (if let Ok(json) = ...) andstd::fs::writefailure (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 valueTake
&Pathinstead of&PathBuffor the parameter.
&PathBufis a common clippyptr_arglint target;&Pathis 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-unavailablenow 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 thebtn-unavailableclass 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 winExtend the bundled-recipe contract test to cover new dashboard/readiness/capability fields.
bundled_recipes_pass_validationvalidates 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.modeis a free-formString;console.rs'ssend_server_commandonly 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_patternis a requiredStringbut nothing asserts it's non-empty; an empty pattern would trivially match the first log line.cap_addaccepts arbitrary Linux capability strings with no allow-list, so a future recipe could request something likeSYS_ADMINwithout any guard rail, even though the current documented need is limited toSYS_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 winKebab-case keyframe names per Stylelint.
fadeUp,fadeIn,slideInLeft,scaleIn,drawPath,ringPulseviolatekeyframes-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-pathrules 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 winLowercase font-family fallback keywords per Stylelint.
BlinkMacSystemFont,SFMono-Regular,Menlotripvalue-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 winDuplicate port/env parsing instead of shared utilities.
getAddress()andloadEnv()reimplementJSON.parseonport_mappings/environmentrather than reusingparsePorts/parseEnvfrom$lib/utils/server, whichFivemDashboard.svelte/MinecraftDashboard.sveltealready 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 winReuse the
getPublicIp()API wrapper instead of a manualinvokecall.
$lib/api/system.tsalready exportsgetPublicIp()for exactly this purpose; duplicating the raw dynamicinvoke("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 winDuplicate port-parsing logic instead of shared
parsePortsutility.Same pattern flagged in
GenericDashboard.svelte; consolidating onparsePortsfrom$lib/utils/serveravoids 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 winCard stats fetched once, not refreshed while server keeps running.
The effect only re-fetches when
server.statustransitions 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 winPrefer
$props.id()overMath.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-hocMath.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 winAdd 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 winConsider 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 valueGradient 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 valueProp interface should be named
ServerConsoleProps.As per coding guidelines, Svelte components should type props with
interface XxxProps { ... }; this uses a plaininterface 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 valueProp interface should be named
ConfigFilesTabProps.As per coding guidelines, Svelte components should type props with
interface XxxProps { ... }; this uses a plaininterface 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 valueHardcoded hex colors bypass the theme system.
The
<option>style uses raw hex (#23272f/#f5f5f6) instead of thecubelit-*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 valueProp interface should be named
ManagedFilesTabProps.As per coding guidelines, Svelte components should type props with
interface XxxProps { ... }; this uses a plaininterface 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
⛔ Files ignored due to path filters (52)
Cargo.lockis excluded by!**/*.lockbun.lockis excluded by!**/*.locksrc-tauri/icons/128x128.pngis excluded by!**/*.pngsrc-tauri/icons/128x128@2x.pngis excluded by!**/*.pngsrc-tauri/icons/32x32.pngis excluded by!**/*.pngsrc-tauri/icons/64x64.pngis excluded by!**/*.pngsrc-tauri/icons/Square107x107Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square142x142Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square150x150Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square284x284Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square30x30Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square310x310Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square44x44Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square71x71Logo.pngis excluded by!**/*.pngsrc-tauri/icons/Square89x89Logo.pngis excluded by!**/*.pngsrc-tauri/icons/StoreLogo.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-hdpi/ic_launcher_round.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-mdpi/ic_launcher_round.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngsrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.pngis excluded by!**/*.pngsrc-tauri/icons/icon.icois excluded by!**/*.icosrc-tauri/icons/icon.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-20x20@1x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-20x20@2x-1.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-20x20@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-20x20@3x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-29x29@1x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-29x29@2x-1.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-29x29@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-29x29@3x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-40x40@1x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-40x40@2x-1.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-40x40@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-40x40@3x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-512@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-60x60@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-60x60@3x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-76x76@1x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-76x76@2x.pngis excluded by!**/*.pngsrc-tauri/icons/ios/AppIcon-83.5x83.5@2x.pngis excluded by!**/*.pngstatic/favicon.pngis excluded by!**/*.png
📒 Files selected for processing (101)
.cursor/.gitignoreCHANGELOG.mdCLAUDE.mdCONTRIBUTING.mdLICENSEREADME.mdSECURITY.mdcrates/cli/Cargo.tomlcrates/cli/src/commands/mod.rscrates/cli/src/commands/server.rscrates/cli/src/commands/smoke.rscrates/cli/src/context.rscrates/cli/src/main.rscrates/cli/src/resolve.rscrates/cli/src/sink.rscrates/cli/tests/integration.rscrates/core/Cargo.tomlcrates/core/src/db/queries.rscrates/core/src/docker/containers.rscrates/core/src/lib.rscrates/core/src/recipes.rscrates/core/src/server/console.rscrates/core/src/server/lifecycle.rscrates/core/src/server/local.rscrates/core/src/server/minecraft.rscrates/core/src/server/mod.rscrates/core/src/server/runner.rscrates/core/src/server/types.rscrates/core/src/server/watchers.rscrates/core/src/smoke.rspackage.jsonsrc-tauri/Cargo.tomlsrc-tauri/icons/android/values/ic_launcher_background.xmlsrc-tauri/icons/icon.icnssrc-tauri/recipes/ark-survival-ascended.jsonsrc-tauri/recipes/ark.jsonsrc-tauri/recipes/cs2.jsonsrc-tauri/recipes/fivem.jsonsrc-tauri/recipes/minecraft-bedrock.jsonsrc-tauri/recipes/minecraft-java.jsonsrc-tauri/recipes/motortown.jsonsrc-tauri/recipes/palworld.jsonsrc-tauri/recipes/project-zomboid.jsonsrc-tauri/recipes/rust-game.jsonsrc-tauri/recipes/terraria.jsonsrc-tauri/recipes/valheim.jsonsrc-tauri/src/commands/file_commands.rssrc-tauri/src/commands/minecraft_commands.rssrc-tauri/src/commands/system_commands.rssrc-tauri/src/lib.rssrc-tauri/tauri.conf.jsonsrc/app.csssrc/lib/api/files.tssrc/lib/api/minecraft.tssrc/lib/api/system.tssrc/lib/components/Button.sveltesrc/lib/components/Card.sveltesrc/lib/components/ConfigFilesTab.sveltesrc/lib/components/ConnRow.sveltesrc/lib/components/CreateProgress.sveltesrc/lib/components/Cube.sveltesrc/lib/components/CubelitCard.sveltesrc/lib/components/DockerOnboarding.sveltesrc/lib/components/GameArt.sveltesrc/lib/components/GameIcon.sveltesrc/lib/components/GameTile.sveltesrc/lib/components/GaugeCard.sveltesrc/lib/components/Input.sveltesrc/lib/components/ManagedFilesTab.sveltesrc/lib/components/Modal.sveltesrc/lib/components/ServerConsole.sveltesrc/lib/components/SkeletonCard.sveltesrc/lib/components/Sparkline.sveltesrc/lib/components/Spinner.sveltesrc/lib/components/StatusPill.sveltesrc/lib/components/StatusRibbon.sveltesrc/lib/components/games/GenericDashboard.sveltesrc/lib/components/games/fivem/FivemDashboard.sveltesrc/lib/components/games/minecraft/MinecraftDashboard.sveltesrc/lib/config/parsers.test.tssrc/lib/config/parsers.tssrc/lib/games/art.tssrc/lib/games/registry.tssrc/lib/stores/settings.svelte.tssrc/lib/stores/theme.svelte.tssrc/lib/types/docker.tssrc/lib/types/recipe.tssrc/routes/+layout.sveltesrc/routes/+page.sveltesrc/routes/create/+page.sveltesrc/routes/server/[id]/+page.sveltesrc/routes/settings/+page.sveltewebsite/.env.examplewebsite/README.mdwebsite/package.jsonwebsite/public/audits/manifest.jsonwebsite/public/audits/v0.2.0.htmlwebsite/src/lib/components/DownloadCTA.sveltewebsite/src/lib/components/Hero.sveltewebsite/src/lib/components/Roadmap.sveltewebsite/src/lib/components/SupportedGames.svelte
💤 Files with no reviewable changes (1)
- src/lib/components/StatusRibbon.svelte
| export type ConfigFormat = "properties" | "ini" | "cfg" | "lua" | "json" | "text"; | ||
|
|
||
| export interface RecipeConfigFile { | ||
| path: string; | ||
| format: ConfigFormat | string; | ||
| label: string; | ||
| fields?: RecipeConfigField[]; | ||
| } |
There was a problem hiding this comment.
📐 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.
| 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.
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
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
config_files/file_tabsmetadata (typed forms with raw-edit fallback; drag-drop upload).<location>/<server-name>.seed_files(pre-boot volume seeding with{ENV}substitution) andcap_add(extra container capabilities).No subscription), which doesn't fit one-click hosting yet.--volume-rootfor placing test volumes on a large disk.Verification campaign (2026-07-02/03, all green)
list✓list✓Info✓players✓status✓listplayers✓Fixes found by the campaign
starting).delete_serverremoves root-owned volume files via a one-shot cleanup container (Valheim, PZ).:latest(documented exception — the game force-updates monthly and moves its glibc floor; the frozenfulltag died withGLIBC_2.34 not found).4.0.1(3.0.6 predates CS2's libv8 dependency and segfaults on boot).Server startup complete) and ARK (server is up).Game.iniediting (the image consumes the file every boot).SYS_PTRACE+ root exec user.Validation
SQLX_OFFLINE=true cargo check --workspace --all-targetsSQLX_OFFLINE=true cargo clippy --workspace --all-targets -- -D warningsSQLX_OFFLINE=true cargo test --workspace(70 tests)bun run check/bun run test(22) /bun run buildcd website && bun run check && bun run buildPre-Merge Requirement
v0.2.0.Summary by CodeRabbit
New Features
Bug Fixes
Documentation