Commit Graph

107 Commits

Author SHA1 Message Date
Anso ca8f22734d fix(auto-update): proxy update execution to remote nodes via Distributed API (#419)
* fix(auto-update): proxy update execution to remote nodes via Distributed API

Remote auto-update policies previously failed because the scheduler tried
to access the Docker daemon directly on remote nodes. Now the scheduler
detects remote nodes and proxies the update execution via HTTP to the
remote Sencho instance's new /api/auto-update/execute endpoint, which
runs image checks and compose updates locally on the remote machine.

* test(auto-update): add getNode mock to NodeRegistry in scheduler tests

The executeUpdate method now calls NodeRegistry.getNode() to detect
remote nodes. The test mock for NodeRegistry was missing this method,
causing the two executeUpdate tests to fail.
2026-04-07 01:44:39 -04:00
Anso 88011e1b16 fix(sidebar): resolve stacks showing unknown status when compose name field is set (#416)
* fix(sidebar): resolve stacks showing unknown status when compose name field is set

The bulk status endpoint matched containers to stacks using the
com.docker.compose.project Docker label, assuming it equals the stack
directory name. When a compose file declares a top-level name: field,
Docker Compose uses that as the project name instead, causing the
label lookup to miss those containers entirely.

The fix parses each stack's compose file to build a project-name-to-
directory mapping (cached with 60s TTL to avoid re-parsing on every
poll), with a fallback to the working_dir label for edge cases.
Also extracts compose file name variants into a shared constant and
fixes an ordering inconsistency in smartFallback.

* docs: add troubleshooting entry for stack status mismatch with name field
2026-04-07 01:44:13 -04:00
Anso 1b890b4d03 fix(fleet): resolve ENOENT when triggering remote node self-update (#413)
The execSync call in SelfUpdateService used cwd from Docker Compose
labels (host-side path) which does not exist inside the container.
Removed cwd (the -f flag provides the absolute compose file path),
added explicit shell, added Docker Compose CLI check at startup,
and improved error capture via stderr.
2026-04-06 23:07:03 -04:00
Anso 8ba4532995 fix(fleet): resolve version detection using package.json over stale generated constant (#410)
* fix(fleet): resolve stuck update states and improve update UX

The fleet node update flow had several bugs: the in-memory update tracker
never cleared terminal states (timeout, failed, completed), leaving nodes
permanently stuck with no way to retry or dismiss. The Recheck button
only re-fetched stale state without clearing it, and the POST trigger
rejected retries with 409 even after timeout.

Backend fixes:
- Add DELETE endpoints (single node + batch) to clear tracker entries
- Fix 409 race: detect expired timeouts and clear terminal states before
  re-triggering
- Populate error messages in the tracker for timeouts and failures
- Include error field in the update-status API response
- Auto-expire completed entries after 60 seconds

Frontend fixes:
- Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges
- Show error details via animated cursor hover (CursorFollow pattern)
- Recheck button now batch-clears all terminal states before fetching
- Recheck shows loading spinner and disables while checking
- Extract NodeCardProps interface for readability

* fix(fleet): detect update completion via process start time

Remote nodes that cannot report their version (e.g. older builds)
caused updates to always time out because completion detection
relied solely on version comparison. The gateway now tracks the
remote node's process start time from /api/meta and detects
container restarts by comparing it across polls.

Also extracts a createTracker() factory to eliminate repeated
object construction across 5 call sites.

* docs: add troubleshooting for first-update timeout on old nodes

Adds a new troubleshooting entry explaining why the first remote
update on nodes running pre-v0.40.0 always times out (neither
version nor process start time can be detected). Documents the
fix: dismiss, recheck, and confirm the node updated.

Also adds a screenshot of the timed-out state with retry/dismiss
buttons to the remote updates feature page.

* fix(fleet): detect update completion via offline detection and error reporting

The update completion detection relied on version change and process
start time, both of which fail on nodes running older Sencho versions
that report "unknown" and lack the startedAt field. This caused every
update to time out after 5 minutes.

Add three-signal detection: version change, process restart (startedAt),
and offline/online detection (node went unreachable during update and
came back). Also add a 90-second early failure heuristic for when the
remote image pull fails silently, and surface pull errors from
SelfUpdateService via /api/meta so the gateway can report them
immediately.

* fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities

Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583.

* fix(deps): bump vite in backend lockfile to resolve audit failures

Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5.

* fix(fleet): resolve version detection using package.json over stale generated constant

resolveVersion() previously returned the build-time SENCHO_VERSION
constant without checking the root package.json. When a branch fell
behind a release-please version bump, the generated constant was stale,
causing remote nodes to show "unknown" version and false "Update
available" badges. The function now walks up to the root package.json
first (authoritative source) and falls back to the generated constant
only if the walk fails.
2026-04-06 22:05:06 -04:00
Anso cc2da99d6f fix(fleet): resolve stuck update states and improve detection (#405)
* fix(fleet): resolve stuck update states and improve update UX

The fleet node update flow had several bugs: the in-memory update tracker
never cleared terminal states (timeout, failed, completed), leaving nodes
permanently stuck with no way to retry or dismiss. The Recheck button
only re-fetched stale state without clearing it, and the POST trigger
rejected retries with 409 even after timeout.

Backend fixes:
- Add DELETE endpoints (single node + batch) to clear tracker entries
- Fix 409 race: detect expired timeouts and clear terminal states before
  re-triggering
- Populate error messages in the tracker for timeouts and failures
- Include error field in the update-status API response
- Auto-expire completed entries after 60 seconds

Frontend fixes:
- Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges
- Show error details via animated cursor hover (CursorFollow pattern)
- Recheck button now batch-clears all terminal states before fetching
- Recheck shows loading spinner and disables while checking
- Extract NodeCardProps interface for readability

* fix(fleet): detect update completion via process start time

Remote nodes that cannot report their version (e.g. older builds)
caused updates to always time out because completion detection
relied solely on version comparison. The gateway now tracks the
remote node's process start time from /api/meta and detects
container restarts by comparing it across polls.

Also extracts a createTracker() factory to eliminate repeated
object construction across 5 call sites.

* docs: add troubleshooting for first-update timeout on old nodes

Adds a new troubleshooting entry explaining why the first remote
update on nodes running pre-v0.40.0 always times out (neither
version nor process start time can be detected). Documents the
fix: dismiss, recheck, and confirm the node updated.

Also adds a screenshot of the timed-out state with retry/dismiss
buttons to the remote updates feature page.

* fix(fleet): detect update completion via offline detection and error reporting

The update completion detection relied on version change and process
start time, both of which fail on nodes running older Sencho versions
that report "unknown" and lack the startedAt field. This caused every
update to time out after 5 minutes.

Add three-signal detection: version change, process restart (startedAt),
and offline/online detection (node went unreachable during update and
came back). Also add a 90-second early failure heuristic for when the
remote image pull fails silently, and surface pull errors from
SelfUpdateService via /api/meta so the gateway can report them
immediately.

* fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities

Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583.

* fix(deps): bump vite in backend lockfile to resolve audit failures

Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5.
2026-04-06 20:09:55 -04:00
Anso a55d1245f8 fix(fleet): resolve version detection pipeline for Docker builds (#402)
* fix(fleet): resolve version detection pipeline for Docker builds

The Dockerfile backend-builder stage was missing a COPY of the root
package.json, causing generate-version.js to fall back to "0.0.0-dev"
at build time. At runtime, the filesystem walk also failed (root
package.json not in the final image), producing the string "unknown"
which the frontend rendered as "vunknown".

Changes:
- Dockerfile: copy root package.json into backend-builder stage
- CapabilityRegistry: return null (not "unknown") for unresolvable
  versions; add isValidVersion() type guard; normalize remote meta
  responses to strip "unknown"/"0.0.0-dev" sentinel values
- Fleet endpoints: hoist gateway version validation outside per-node
  loops; treat unresolvable remote versions as "potentially outdated"
  instead of silently marking them up to date
- FleetView: guard all version display points (card badge, update
  button, gateway label, modal columns) via shared formatVersion()
- EditorLayout, CapabilityGate: use shared isValidVersion utility
- New frontend/src/lib/version.ts shared utility
- Docs: add troubleshooting section for version display edge cases
- Screenshots: updated Fleet Overview and Node Updates modal

* docs: update fleet node updates screenshot with live remote node
2026-04-06 03:03:56 -04:00
Anso 670a429168 fix(fleet): resolve getSenchoVersion crash in Docker containers (#396)
* fix(fleet): resolve getSenchoVersion crash in Docker containers

Replace the runtime __dirname walk (which fails in Docker because the
root package.json is absent from the final image) with a build-time
version injection. A prebuild script reads the root package.json and
generates src/generated/version.ts with the version baked in as a
constant. The compiled output carries the correct version regardless
of the container's filesystem layout.

Retains a filesystem walk fallback for dev environments where the
prebuild hook may not have run.

* fix(fleet): skip postinstall version generation when script is absent

The Docker prod-deps stage copies only package.json (no source files)
before running npm ci --omit=dev. The postinstall hook now checks for
the script file before executing, so it no-ops in stages where
scripts/generate-version.js has not been copied.
2026-04-06 00:44:36 -04:00
Anso d437a195b6 fix(fleet): resolve getSenchoVersion crash in Docker containers (#391)
getSenchoVersion() used require('../../../package.json') which resolves
correctly in dev (src/services/ -> 3 up -> root) but fails in Docker
where the compiled file is at dist/services/ (2 levels from /app/).
The 500 error on /api/meta caused fetchRemoteMeta to always return
empty capabilities, blocking all capability-gated features on remote
nodes. Replace with a __dirname-based walk that finds the root
package.json (name === 'sencho') regardless of directory depth.
2026-04-05 23:31:33 -04:00
Anso 9e0c9d3f2d fix(licensing): backward-compatible tier/variant enforcement and self-healing variant detection (#385)
Accept legacy tier ('pro') and variant ('personal', 'team') names from older
remote nodes, normalizing them to current values ('paid', 'skipper', 'admiral')
in authMiddleware. This fixes distributed license enforcement failing between
v0.38.3 and v0.38.0 nodes due to the tier rename in v0.38.1.

Also fixes:
- Self-healing getVariant() that cross-checks stored variant_type against
  product/variant name metadata on every call, correcting stale cached values
  from previous buggy resolution logic
- Unguarded API responses in ResourcesView causing potential t.map crashes
- Fleet update status now polls on a 120s interval (was only fetched on mount)
- fetchRemoteMeta failures now logged for diagnosability
2026-04-05 21:50:34 -04:00
Anso b08f698e8f fix(licensing): resolve variant from product_name when variant_name lacks tier info (#382)
Lemon Squeezy variant names describe billing periods ("Lifetime",
"Monthly", "Annual") without containing the tier name. resolveVariantType
now checks both variant_name and product_name (e.g. "Sencho Admiral") to
correctly identify the tier. Fixes Admiral licenses being misidentified
as Skipper when the LS variant name is just "Lifetime".
2026-04-05 19:56:59 -04:00
Anso 797623e56f fix(licensing): rename variant values to skipper/admiral and store resolved type (#379)
Rename internal variant values from 'personal'/'team' to 'skipper'/'admiral',
aligning code with user-facing tier names. Variant type is now resolved once
at activation/validation and stored in DB via license_variant_type, instead
of string-matching the Lemon Squeezy variant_name on every read. Also captures
variant_id for future lookups. Pre-existing installs auto-migrate on first
getVariant() call.
2026-04-05 18:45:57 -04:00
Anso f841c402b2 fix(licensing): resolve Admiral variant detection and lifetime license handling (#376)
* fix(licensing): resolve Admiral variant detection and lifetime license handling

The Lemon Squeezy variant name for Admiral licenses contains "Admiral"
(not "Team"), but getVariant() only checked for "team" and "personal".
This caused Admiral licenses to be misidentified as Skipper, locking all
Admiral-exclusive features.

- Map "admiral" variant names to internal "team" value, "skipper" to "personal"
- Add isLifetime field to LicenseInfo API response
- Hide "Manage Subscription" button for lifetime licenses (no billing portal)
- Show "Duration: Lifetime" instead of empty renewal date
- Hide upgrade cards for active Admiral users
- Add 23 unit tests covering variant resolution, tier computation, and lifetime detection
- Add troubleshooting entries for wrong tier label, locked features, and billing portal errors

* fix(licensing): address code review findings

- Fix nested ternary in LicenseSection JSX; restore conditional rendering
  to avoid showing an empty "N/A" row for non-subscription states
- Clean up test file: use shared svc variable, remove redundant comments,
  add trialDaysRemaining assertions, rename describe block
2026-04-05 07:00:21 -04:00
Anso f516275834 refactor(licensing): replace Pro branding with Community/Skipper/Admiral tiers (#375)
Eliminate all references to "Pro" across backend, frontend, and docs.
Internal tier value renamed from 'pro' to 'paid'; user-facing text now
uses the thematic tier names (Community, Skipper, Admiral).

- Rename LicenseTier 'pro' to 'paid' in backend and frontend types
- Rename requirePro guard to requirePaid, error code PRO_REQUIRED to PAID_REQUIRED
- Rename ProGate.tsx to PaidGate.tsx with updated copy
- Fix: trial users can now see upgrade/purchase cards in Settings
- Update all docs and openapi.yaml to use correct tier names
2026-04-05 05:59:36 -04:00
Anso 2ee959ec3b feat(dashboard): redesign as DevOps command center (#371)
* feat(dashboard): redesign as DevOps command center

Transform the dashboard from a basic stats viewer into a high-signal
operational command center with 5 composable sections:

- Health status bar with system health derivation (Healthy/Degraded/Critical)
- Resource gauges with visual progress bars and threshold coloring
- Paginated stack health table with per-stack UP/DN, CPU, memory, and
  click-to-navigate (8 per page)
- Enhanced historical CPU/RAM charts with skeleton empty states
- Recent alerts feed with severity-coded notifications

Extract monolithic HomeDashboard.tsx (447 lines) into composable
sub-components under dashboard/ directory. Remove Docker Run to Compose
converter from the landing surface. Add defensive .ok check on container
status fallback in EditorLayout.

* feat(dashboard): add Clear All Notifications button to Recent Alerts

Add a destructive ghost button below the alerts feed that calls
DELETE /api/notifications to clear all notifications, then refreshes
the list. Button only appears when there are alerts to clear.

* feat(dashboard): add pagination to Recent Alerts section

Same pattern as Stack Health table: 8 items per page with prev/next
chevron controls and page indicator in the card header. Pagination
auto-hides when there are 8 or fewer alerts. Page resets on clear all.

* fix(dashboard): resolve container count oscillation and add cursor hover detail

Fix container stats flickering between 0 and correct values by moving
state resets to the top of each useEffect body (runs once per node
switch, not on every poll tick). Add animate-ui cursor primitive and
wire it to the active containers number in ResourceGauges to show
managed/external breakdown on hover. Silence noisy Docker socket
errors when engine is unreachable.

* feat(dashboard): add cursor hover to health status with reason breakdown

Wrap the health badge (pulsing dot + label) in a CursorFollow tooltip
that explains why the node is Critical, Degraded, or Healthy. Shows
specific metrics (e.g. "RAM at 97.9%", "Disk at 96.4%") when hovered.
Displays "All systems nominal" for healthy nodes.

* fix(dashboard): resolve OOM from unbounded Docker stats polling

Three root causes addressed:

1. updateGlobalDockerNetwork had no overlap guard. When Docker was slow,
   3-second interval ticks stacked up, creating dozens of concurrent
   container.stats() calls that exhausted the heap. Added isUpdatingNetwork
   flag and increased interval from 3s to 5s.

2. Historical metrics query returned ~20K rows (1-minute buckets x 14
   containers x 24h). Downsampled to 5-minute buckets, reducing response
   size by ~5x.

3. Dashboard polling continued when the browser tab was hidden, creating
   phantom load. Replaced setInterval with visibilityInterval helper that
   pauses polling on tab hide and resumes with an immediate fetch on focus.

* fix(dashboard): use loadFile for stack navigation from Stack Health table

The onNavigateToStack callback was only calling setSelectedFile and
setActiveView, skipping the full load flow (YAML content, env files,
containers, backup info). This caused the editor to show stale state
with a "Start" button for running stacks and empty YAML. Now calls
loadFile() which is the same path the sidebar uses.

* refactor(dashboard): simplify Containers card layout

Replace 2-column grid with vertical layout matching other gauge cards.
Active count uses text-2xl hero number, exited count sits in subtitle
position. Removed redundant total count row.

* fix(dashboard): unify notification types and fix multi-node clear

- Replace duplicate Notification interface in EditorLayout with shared
  NotificationItem from dashboard/types.ts
- Tighten is_read type from number | boolean to number (matches SQLite)
- Pass notifications from EditorLayout (which aggregates all nodes) to
  HomeDashboard as props, removing duplicate local-only polling from
  useDashboardData
- Fix Clear All to use clearAllNotifications (deletes from all nodes)
  instead of fetchNotifications (which was just a re-fetch, causing
  remote notifications to reappear immediately after clearing)
- Delegate DELETE responsibility from RecentAlerts to parent handler

* fix(dashboard): handle optional nodeId in notification operations

Guard against undefined nodeId when calling fetchForNode for mark-read,
delete, and clear-all notification operations. The shared NotificationItem
type has nodeId as optional since the API response doesn't include it;
EditorLayout enriches it but TypeScript correctly flags the possibility.
2026-04-04 02:51:42 -04:00
Anso 55d3b8ca1d feat(stacks): state-aware sidebar context menu and Open App action (#368)
* feat(stacks): state-aware sidebar context menu and Open App action

- Context menu now adapts to stack state: running stacks show
  Stop/Restart/Update, stopped stacks show Deploy only
- Added "Open App" shortcut to open a stack's web UI directly
  from the sidebar (visible when running with a published port)
- Backend bulk status endpoint enriched with mainPort detection
- Reduced manual image update check cooldown from 10 to 2 minutes
- Rate limit error message now derives from the configured constant

* fix(stacks): use const for bulkPorts (prefer-const lint)
2026-04-03 21:41:01 -04:00
Anso f9ebd1d77c feat: UI polish sprint — 7 items + logs toolbar redesign (#365)
* feat: UI polish sprint — tag filters, toast tokens, logs toolbar, billing portal, editor UX

- Fleet: replace inline tag pills with multi-select combobox dropdown
- Toast: remap all notification colors to oklch design tokens
- Logs: convert floating hover toolbar to permanent pinned toolbar,
  replace all hardcoded colors with design tokens for theme support
- Audit: fix dropdown scroll-lock (modal=false), light theme button contrast
- Resources: remove redundant inner border/bg on tab wrapper
- Editor: add ⌘K/Ctrl+K shortcut hint and handler, standardize button heights
- Billing: signed Lemon Squeezy portal URLs via sencho.io proxy for all tiers
- New MultiSelectCombobox UI component

* feat(editor): replace button row with split-button dropdown

Replace three separate editor buttons (Discard, Save Only, Save & Deploy)
with a compact split-button dropdown. Primary action is "Save & Deploy";
chevron opens dropdown with "Save Only" and "Discard Changes" options.
2026-04-03 20:33:44 -04:00
Anso dfd4d2858a feat(stacks): per-stack action tracking, optimistic status, and bulk status endpoint (#362)
* feat(stacks): per-stack action tracking, optimistic status, and bulk status endpoint

Replace global loadingAction mutex with per-stack tracking so users can
fire actions on multiple stacks concurrently. Add optimistic status
updates to fix sidebar showing "--" after stop/start. Add bulk
GET /api/stacks/statuses endpoint using a single docker.listContainers
call instead of N docker compose ps invocations (~21s → ~110ms for 3
stacks). Falls back to per-stack queries for remote nodes on older
versions.

* fix(stacks): remove stale 'start' action check from deploy button label
2026-04-03 17:19:10 -04:00
Anso 6c26ae3f50 feat(license): distributed license enforcement across multi-node setups (#359)
* feat(license): distributed license enforcement across multi-node setups

The primary instance's license tier is now asserted to remote nodes on
every proxied HTTP and WebSocket request via trusted headers. Remote
nodes honor the assertion only when the request carries a valid
node_proxy JWT, preventing unauthorized elevation from browsers or API
tokens. Falls back to local license tier for direct access.

* fix(test): remove unused vi import in distributed-license tests
2026-04-03 11:31:14 -04:00
Anso 4fe4ac5d19 fix(db): recreate stack_update_status table with composite primary key (#356)
The original table used stack_name as the sole PRIMARY KEY. A later
migration added node_id and a composite unique index, but the old
single-column PK was never removed (SQLite doesn't support DROP
CONSTRAINT). This caused UNIQUE constraint failures when two nodes
shared the same stack name.

Recreate the table with PRIMARY KEY (node_id, stack_name) for existing
databases, and fix the CREATE TABLE for new databases.
2026-04-03 02:03:40 -04:00
Anso 87b5908288 feat(fleet): add remote node update management (#353)
Add the ability to check for outdated nodes and trigger over-the-air
updates from Fleet View. Nodes self-update by pulling the latest Docker
image and recreating their container via the "last breath" pattern.

Backend:
- SelfUpdateService: self-container identification via HOSTNAME + Docker
  Compose labels, triggers pull + force-recreate
- CapabilityRegistry: runtime capability disabling via disableCapability()
- POST /api/system/update (202 + deferred self-update)
- GET /api/fleet/update-status (version comparison across fleet)
- POST /api/fleet/nodes/:nodeId/update (single node)
- POST /api/fleet/update-all (bulk remote update)
- In-memory update tracker with 5-min timeout

Frontend:
- Node Updates modal with summary stats, search filter, table layout,
  per-node Update buttons, and bulk Update All
- Version badges and update-available indicators on node cards
- ReconnectingOverlay for local node updates (polls /api/health)
- 5s fast-poll when any node is actively updating
- UpdateStatusBadge shared component for consistent badge rendering

Requires Skipper (Pro) tier. Nodes must be deployed via Docker Compose
with Docker socket access.
2026-04-03 01:39:22 -04:00
Anso ee75811e25 feat(nodes): add capability-based node compatibility negotiation (#350)
* feat(nodes): add capability-based node compatibility negotiation

Each Sencho instance now exposes /api/meta with its version and supported
capabilities. When the user switches nodes, the frontend fetches this
metadata and disables features the remote node doesn't support via a
CapabilityGate overlay. Version is shown in the node switcher dropdown
and connection test results.

- Backend: CapabilityRegistry with static capability list and fetchRemoteMeta helper
- Backend: /api/meta (public) and /api/nodes/:id/meta (auth) endpoints
- Frontend: NodeContext enhanced with per-node meta caching (5min TTL)
- Frontend: CapabilityGate component with typed Capability union
- Frontend: 13 features wrapped with capability gates
- Docs: node-compatibility.mdx + OpenAPI spec updates

* fix(nodes): revert to require() for package.json version reading

The static import fails in the Docker multi-stage build because the
root package.json is not copied into the backend-builder stage. The
require() call resolves at runtime when the file is available.
2026-04-03 00:06:34 -04:00
Anso 1b573f542a feat(notifications): add shared notification routing rules (Admiral tier) (#347)
Route stack alerts to specific Discord, Slack, or webhook channels instead
of the single global endpoint. Includes per-rule enable/disable, priority
ordering, and automatic fallback to global agents when no rule matches.

- Add notification_routes table, interface, and CRUD in DatabaseService
- Add routing logic in NotificationService.dispatchAlert with optional stackName
- Pass stack context from MonitorService (crash/health) and SchedulerService
- Add 5 API endpoints gated with requireAdmin + requireAdmiral
- Add NotificationRoutingSection UI with Combobox stack picker, channel tabs
- Parallel webhook dispatch via Promise.allSettled
- 10 unit tests covering routing, fallback, and edge cases
- Documentation with screenshots at docs/features/notification-routing.mdx
2026-04-02 22:22:27 -04:00
Anso efbd20fed5 feat(nodes): add per-node scheduling and update visibility (#344)
* feat(labels): add stack_labels schema and DatabaseService CRUD methods

* feat(labels): add label CRUD, assignment, and bulk action API routes

* feat(labels): add oklch label color palette for light and dark themes

* feat(labels): add LabelPill and LabelDot reusable components

* feat(labels): add LabelAssignPopover component for inline label management

* feat(labels): add label pill bar, label dots, and label assignment to sidebar

* feat(labels): add label filtering and label dots to fleet view

* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)

* docs: add Stack Labels feature documentation

* fix(labels): use context menu sub-menu for label assignment and add settings integration

Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.

* docs: update stack labels documentation with screenshots and corrected instructions

* fix(labels): address security and quality issues from code review

- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system

* chore: update CHANGELOG with stack labels feature

* feat(nodes): add per-node scheduling and update visibility

Add Schedules and Updates columns to the Nodes table showing active
task counts, next run times, and auto-update status per node. A calendar
action button navigates to filtered schedule/auto-update views.

Backend changes:
- Add node_id to stack_update_status table (migration + unique index)
- Cascade cleanup on node deletion (scheduled_tasks + update status)
- Pre-check target node existence/status before executing scheduled tasks
- New GET /api/nodes/scheduling-summary endpoint
- New GET /api/image-updates/fleet endpoint with 2-minute cache
- Parallelize remote node fetches with Promise.allSettled
- Wrap deleteNode cascade in a transaction

Frontend changes:
- NodeManager: Schedules/Updates columns with summary data fetch
- EditorLayout: sencho-navigate event listener for cross-component nav
- ScheduledOperationsView/AutoUpdatePoliciesView: filterNodeId prop,
  filter bar UI, pre-selected node in create dialog
2026-04-02 20:37:53 -04:00
Anso 28e7be652c feat(labels): add stack labels for organizing, filtering, and bulk actions (#341)
* feat(labels): add stack_labels schema and DatabaseService CRUD methods

* feat(labels): add label CRUD, assignment, and bulk action API routes

* feat(labels): add oklch label color palette for light and dark themes

* feat(labels): add LabelPill and LabelDot reusable components

* feat(labels): add LabelAssignPopover component for inline label management

* feat(labels): add label pill bar, label dots, and label assignment to sidebar

* feat(labels): add label filtering and label dots to fleet view

* feat(labels): add label-scoped bulk actions (deploy/stop/restart all)

* docs: add Stack Labels feature documentation

* fix(labels): use context menu sub-menu for label assignment and add settings integration

Replace broken Popover-inside-ContextMenu pattern with native Radix
ContextMenuSub for reliable label toggling on right-click. Wrap
ContextMenuSubContent in a Portal to prevent overflow clipping. Add
"Manage labels..." item that opens Settings directly to Labels section.
Fix close button overlap in LabelsSection header. Add LabelsSection
settings component with full CRUD, assignment counts, and ProGate.
Add initialSection prop to SettingsModal for deep-linking. Include
screenshots for documentation.

* docs: update stack labels documentation with screenshots and corrected instructions

* fix(labels): address security and quality issues from code review

- Add NaN validation on parseInt(req.params.id) in label routes
- Scope updateLabel/deleteLabel by nodeId to prevent cross-node IDOR
- Validate labelIds belong to correct node in setStackLabels
- Add requireAdmin check on bulk action endpoint
- Replace error: any with error: unknown and proper narrowing
- Remove unused Label import from index.ts
- Remove unused isPro prop from LabelsSection
- Add strokeWidth={1.5} to Check icons per design system

* chore: update CHANGELOG with stack labels feature
2026-04-02 19:25:43 -04:00
Anso 4488637656 feat(resources): add network management with create, inspect, and topology visualization (#335)
* feat(resources): add network management with create, inspect, and topology visualization

Add full Docker network CRUD: create networks with custom drivers, subnets,
and IPAM config; inspect network details including connected containers and
IP addresses; interactive topology graph visualization (Pro-gated to
Skipper/Admiral tiers). Includes backend routes, DockerController methods,
unit tests, documentation with screenshots, and updated changelog.

* refactor(resources): address code review findings for network management

- Add batch GET /api/system/networks/topology endpoint to eliminate N+1
  HTTP calls from the topology view
- Export DockerNetwork type from ResourcesView, remove duplicate in
  NetworkTopologyView
- Wire up isInspectLoading state to Eye button (spinner + disabled)
- Remove unnecessary wrapper div around FilterToggle
- NetworkTopologyView is now self-contained (fetches from batch endpoint,
  no longer needs networks prop)

* fix(resources): align pre-existing UI with design system standards

- Replace hardcoded red-500 on image/volume delete buttons with
  text-destructive/60 hover:bg-destructive tokens
- Replace shadow-sm with shadow-card-bevel on all cards (Disk Footprint,
  Quick Clean, Resource Tabs, Unmanaged Containers)
- Add strokeWidth={1.5} to all action button Trash2 icons
- Type network drivers as union type instead of raw strings (backend
  NetworkDriver type + frontend NETWORK_DRIVERS constant)

* fix(resources): add generic type args to useNodesState/useEdgesState

Fixes TS2345 build error in CI where tsc -b (strict mode via
tsconfig.app.json) infers never[] from untyped empty array literals
passed to React Flow hooks.

* fix(resources): replace explicit any in catch blocks with unknown narrowing

ESLint no-explicit-any errors in CI for three catch blocks added by the
network management feature.
2026-04-02 14:53:11 -04:00
Anso 10597d213a fix(error-handling): surface silent errors across the codebase (#326)
Add console.warn/console.error logging to 22 silent catch blocks across
10 files. Errors in cleanup, migrations, SSO, fleet snapshots, shutdown,
and validation are now visible in logs. ENOENT guards added to
file-system catches to distinguish missing files from permission errors.
No control flow changes.
2026-04-01 21:56:41 -04:00
Anso f317a83814 fix(security): harden encryption key permissions, increase password minimum, remove sensitive logs (#323)
Self-heal encryption key file permissions to 0600 on startup. Increase
minimum password length from 6 to 8 characters per NIST SP 800-63B.
Remove console.log statements that exposed file paths, .env locations,
stack names, and admin usernames to stdout.
2026-04-01 21:27:37 -04:00
Anso 2d6b4c233d fix(security): pre-launch security hardening audit & remediation (#320)
- Webhook HMAC: capture raw request bytes via express.json verify callback
  instead of re-serializing with JSON.stringify
- AES-256-GCM: use NIST-recommended 12-byte IV (backward compatible with
  existing 16-byte IVs)
- Node proxy tokens: add 1-year default expiry (previously no expiry)
- Host console env filtering: pattern-based approach blocking SECRET,
  PASSWORD, TOKEN, KEY, CREDENTIAL keywords (previously only 4 explicit keys)
- CORS: deny cross-origin requests when FRONTEND_URL is unset in production
  (previously fell back to allowing all origins)
2026-04-01 20:50:43 -04:00
Anso 28c7a8fd54 feat(auto-update): add auto-update policies and fix image update detection (#297)
* feat(auto-update): add auto-update policies and fix image update detection

Auto-Update Policies (Skipper+ tier):
- New scheduled task action type 'update' for check-then-update flow
- Dedicated AutoUpdatePoliciesView with CRUD, cron presets, and run history
- Conditional tier gating: Skipper gets auto-update, Admiral gets full scheduled ops
- Backend executeUpdate: checks digests, pulls only if newer, atomic redeploy

Image Update Detection fixes (all tiers):
- Fix stack name key mismatch: use working_dir label instead of project label
- Add 5-minute periodic frontend polling for background check results
- Replace fixed 3s timeout with polling-based manual refresh via /api/image-updates/status
- Clear update status after successful stack update

* fix(ui): remove Skipper tier badge from Auto-Update Policies header

* fix(ui): remove auto-update action from Scheduled Operations view

Admiral users have a dedicated Auto-Update view — showing update tasks
in Scheduled Operations too was confusing duplication. Each view now
owns a distinct, non-overlapping set of action types.

* fix(auto-update): fix node-stack linking and add All Stacks option

- Stack dropdown now re-fetches when node selection changes using
  fetchForNode, and resets the selected stack
- Node selector moved above stack selector with stack disabled until
  a node is picked
- Added "All Stacks" wildcard option that checks and updates every
  stack on the selected node
- Backend executeUpdate refactored to iterate over all stacks when
  target_id is "*", with per-stack error isolation

* refactor(ui): replace Select dropdowns with searchable Combobox component

Add a reusable Combobox component with inline search and use it for
Node/Stack selectors in both Auto-Update Policies and Scheduled
Operations dialogs. Also fixes node-stack linking bug where changing
node didn't update the stack list.

* fix(ui): resolve CI TypeScript errors in Combobox and ScheduledOperationsView

Add missing searchPlaceholder prop to ComboboxProps interface and remove
dead 'update' action filter that conflicted with the narrowed type union.

* fix(ui): use Geist Sans font in toast component

The toast renders via React portal on document.body, bypassing the app's
font inheritance. Add explicit font-family declaration using var(--font-sans)
to match Sencho's design system.
2026-04-01 00:22:47 -04:00
Anso eccdd1b879 feat(scheduled-ops): add failure notifications, granular targeting, and history export (#286)
- Dispatch error alerts via NotificationService when scheduled tasks fail,
  with info-level recovery notifications when a previously-failing task succeeds
- Per-service restart targeting: scheduled stack restarts can target specific
  services instead of restarting the entire stack
- Prune label filter: scheduled prune operations can be scoped to resources
  matching a specific Docker label
- CSV export button in the execution history panel for one-click download
- Fix: prune_targets was silently dropped on task creation (missing in INSERT)
2026-03-31 09:51:53 -04:00
Anso 10d16361fa fix(stacks): avoid resource busy error in Docker fallback deletion (#271)
The previous command `rm -rf /cleanup` tried to remove the bind mount
point itself, which the kernel rejects with EBUSY. Changed to
`find /cleanup -mindepth 1 -maxdepth 1 -exec rm -rf {} +` which
removes all contents without touching the mount point. The existing
fsPromises.rmdir() call then cleans up the empty host directory.
2026-03-29 23:52:40 -04:00
Anso 116f15dae9 fix(stacks): resolve permission denied error on stack deletion (#261)
* fix(stacks): resolve permission denied error when deleting stacks with root-owned files

When Docker Compose creates files as root inside a stack directory, the
non-root Sencho process cannot remove them. This adds a Docker-based
fallback: if fsPromises.rm fails with EACCES/EPERM, Sencho spawns a
short-lived Alpine container to clean up the root-owned files.

Also enhances docker compose down with --volumes --remove-orphans to let
Docker clean up its own resources before filesystem deletion.

* docs: clarify that pre-existing root-owned stacks can be deleted

* fix(stacks): include Docker stderr in fallback deletion error message

Fixes CI lint failure: 'stderr' was assigned but never read in
forceDeleteViaDocker(). Now surfaces Docker stderr output in the error
message when the fallback cleanup fails.
2026-03-29 21:32:05 -04:00
Anso d586ce393a feat(audit-log): add configurable retention, export, Auditor role, and enhanced filtering (#258)
- Configurable retention: audit_retention_days setting (1-365 days, default 90)
  replaces hardcoded 90-day retention, exposed in Settings > Data Retention
- Export: one-click CSV/JSON export of filtered audit data via new
  GET /api/audit-log/export endpoint (capped at 10,000 entries)
- Auditor role: read-only role with system:audit permission for viewing
  and exporting audit logs without admin privileges (Admiral tier)
- Enhanced filtering: full-text search across summaries/paths/usernames,
  date range picker, and expandable row details showing request path,
  IP address, node ID, and entry ID
2026-03-29 20:18:51 -04:00
Anso c328b7f49a refactor: rename Personal Pro to Skipper and Team Pro to Admiral (#256)
Align paid tier names with Sencho's nautical identity. Internal variant
values ('personal'/'team') remain unchanged in code, database, and
Lemon Squeezy integration — only user-facing display names updated.

- Backend: requireTeamPro → requireAdmiral, TEAM_PRO_REQUIRED → ADMIRAL_REQUIRED
- Frontend: TeamProGate.tsx → AdmiralGate.tsx, TierBadge labels updated
- Website: PricingSection tier names and nautical descriptions
- Docs: all 11 affected pages renamed, nautical footnote added to licensing
2026-03-29 18:00:29 -04:00
Anso 8380fbad4b feat(rbac): add Deployer & Node Admin roles with scoped permissions (Team Pro) (#253)
* feat(rbac): add Deployer & Node Admin roles with scoped permissions (Team Pro)

Add intermediate RBAC roles gated to Team Pro tier:
- Deployer: can deploy/restart/stop/start stacks but cannot edit compose files, delete stacks, or access system settings
- Node Admin: full stack and node management within scope, no system settings access
- Scoped permissions: assign roles per-stack or per-node for fine-grained access control
- Permission engine with checkPermission/requirePermission guards replacing requireAdmin on stack/node routes
- Frontend can() function with /api/permissions/me endpoint for client-side permission checks
- User management UI updated with 4-role selector and scoped permission editor
- Documentation updated with permission matrix, scoped permission docs, and screenshots

* fix(rbac): remove unused RoleAssignment import to fix lint error
2026-03-29 17:02:56 -04:00
Anso 244c83a0c3 feat(registries): add private registry credential management (Team Pro) (#240)
Add centralized credential storage for private Docker registries with
support for Docker Hub, GHCR, AWS ECR, and self-hosted registries.

- New `registries` table with AES-256-GCM encrypted secrets
- RegistryService with CRUD, test connectivity, Docker config generation
- 5 API endpoints gated by requireTeamPro + requireAdmin
- ComposeService injects credentials via temp DOCKER_CONFIG on deploy/pull
- ImageUpdateService passes stored credentials for private registry checks
- AWS ECR just-in-time token refresh via @aws-sdk/client-ecr
- RegistriesSection UI in Settings Hub with type-aware form
- Documentation with screenshots
2026-03-29 12:56:30 -04:00
Anso 330eec4bff fix(scheduled-ops): audit log text, run attribution, prune targets, and pagination (#234)
- Fix "Run Now" audit log showing "Created scheduled task" instead of "Triggered scheduled task" by adding wildcard-based route matching with specificity sorting
- Add triggered_by column to track whether runs were started by the scheduler or manually via Run Now
- Add configurable prune targets (containers, images, networks, volumes) with checkbox UI
- Add pagination to execution history with offset-based navigation
- Document Run Now behavior on disabled tasks and add screenshots
2026-03-29 01:01:45 -04:00
Anso 31e1795af0 feat(scheduled-ops): add scheduled operations for Team Pro users (#231)
Adds the ability to schedule recurring Docker operations (stack restarts,
fleet snapshots, system prunes) via cron expressions with full execution
history logging. Includes Run Now for on-demand execution.
2026-03-28 23:56:37 -04:00
Anso 8d8118c963 feat(api-tokens): add scoped API tokens for CI/CD automation (Team Pro) (#220)
Add long-lived API tokens with three permission scopes (read-only,
deploy-only, full-admin) for CI/CD pipelines, scripts, and automation.

- Database: api_tokens table with SHA-256 hashed storage
- Auth: extend middleware to authenticate Bearer API tokens
- Scope enforcement: middleware restricts actions per token scope
- API: CRUD endpoints gated behind Team Pro + admin
- UI: ApiTokensSection in Settings Hub with create/revoke/copy flows
- Docs: new api-tokens.mdx with usage examples and screenshots
2026-03-28 15:37:54 -04:00
Anso f99abe907d fix(license): default 14-day trial to Personal Pro instead of Team Pro (#216)
Trial users now get Personal Pro features only. Team Pro features
(SSO, audit log, unlimited accounts) require a Team Pro license.
2026-03-28 04:23:28 -04:00
Anso bd4008f509 feat: SSO & LDAP authentication for Team Pro (#209)
* feat: SSO & LDAP authentication for Team Pro

Add SSO integration allowing Team Pro users to authenticate via LDAP/Active Directory, Google, GitHub, and Okta identity providers. SSO works alongside password authentication with auto-provisioning and role mapping.

- LDAP bind+search authentication with group-based role mapping
- OIDC/OAuth2 flows with PKCE and CSRF protection for Google, GitHub, Okta
- Auto-provisioning: first SSO login creates a Sencho account automatically
- Role mapping via LDAP group membership or OIDC JWT claims
- SSO settings UI in Settings → SSO with per-provider config and test connection
- SSO login buttons on login page with LDAP toggle
- Environment variable seeding for infrastructure-as-code workflows
- Secrets encrypted at rest via CryptoService (AES-256-GCM)
- Seat limit enforcement during auto-provisioning
- Full documentation: feature docs, quickstart guides, env var reference

* fix: resolve ESLint errors in SSO feature

- Remove unnecessary escape characters in regex character classes
- Remove unused `issuer` variable from OIDC callback handler
- Fix setState-in-effect lint error in Login.tsx by using useState initializer
- Suppress set-state-in-effect for SSOSection fetch pattern (matches existing codebase convention)
2026-03-28 03:30:01 -04:00
Anso 1799030060 feat: audit logging, secrets at rest, and legacy cleanup (#205)
* feat: audit logging, secrets at rest, and legacy cleanup

- Add Team Pro audit log: records all mutating API actions with user
  attribution, searchable timeline UI with filtering and pagination
- Add AES-256-GCM encryption at rest for node API tokens via CryptoService
- Drop 9 legacy SSH/TLS columns from nodes table (dead since v0.7)
- Remove orphaned MaintenanceModal.tsx (dead code, never imported)
- Add requireTeamPro backend guard for team-tier features
- Add audit log cleanup (90-day retention) to MonitorService
- Add docs page and navigation entry for audit log feature

* fix: remove unused AUTH_TAG_LENGTH constant from CryptoService
2026-03-28 01:01:08 -04:00
Anso 32a7d53b2b feat: RBAC, atomic deployments, fleet backups, and licensing (Pro) (#185)
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)

Introduces three Pro-tier features:

- RBAC: Multi-user system with admin/viewer roles, user management UI,
  automatic migration from single-admin credentials, viewer restrictions
  across the entire UI (read-only editor, hidden action buttons)

- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
  rollback on health probe failure, manual rollback button, health probes
  added to stack updates, webhook-triggered deploys use atomic rollback

- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
  nodes (local + remote), stored centrally in SQLite, per-stack restore
  with optional redeploy, graceful handling of offline nodes

* fix(settings): use correct ProGate prop name in UsersSection

* fix(settings): remove unused isPro prop from UsersSection

* fix(auth): fetch user info after login and setup so isAdmin is set correctly

* feat(pricing): revise pricing strategy and enforce variant-based seat limits

Raise Personal Pro from $49/yr to $69/yr with 3 viewer seats (up from 1).
Add $15/mo billing option for Team Pro. Mark lifetime pricing as a
90-day early-adopter offer. Store Lemon Squeezy variant_name on
activation/validation and enforce seat limits server-side per variant.

* feat(licensing): add Lemon Squeezy checkout, webhook, and billing portal integration

Server-side checkout URL generation (POST /api/checkout) with admin email
pre-fill and instance_id custom data. HMAC-SHA256 verified webhook endpoint
(POST /api/webhooks/lemonsqueezy) handling order, subscription, and payment
lifecycle events for automatic license activation. Customer billing portal
link stored from webhook events and exposed via GET /api/billing/portal.
In-app checkout buttons in Settings with manual license key fallback.

* fix(licensing): exempt Lemon Squeezy webhook from auth middleware

The catch-all auth middleware on /api/* was blocking the public webhook
endpoint. Added /webhooks/lemonsqueezy to the exemption list alongside
/auth/* and /webhooks/:id/trigger.

* feat(pricing): update pricing to final live rates

Personal Pro: $7.99/month, $69.99/year, $249 lifetime.
Team Pro: $49.99/month, $499.99/year, $1,499 lifetime.
Added personal_monthly checkout variant across backend, frontend, and website.

* refactor(licensing): remove server-side checkout/webhook for self-hosted model

Sencho is self-hosted — each user runs their own instance, so there is
no central server to receive webhooks or hold the store API key. Replaced
in-app checkout buttons with a "View Pricing" redirect to sencho.io and
kept manual license key activation as the primary flow.

- Delete LemonSqueezyService (checkout, webhook, HMAC verification)
- Remove POST /api/checkout, GET /api/billing/portal, POST /api/webhooks/lemonsqueezy
- Remove raw body parser and auth exemption for webhook route
- Remove all LEMONSQUEEZY_* env vars from .env.example
- Replace checkout buttons in SettingsModal with single "View Pricing" button
- Simplify LicenseContext checkout to open sencho.io pricing page
- Update licensing docs to reflect website-based purchase flow

* chore: normalize em-dashes to hyphens across codebase (linter)

* chore: remove accidentally tracked directories from index
2026-03-26 21:58:24 -04:00
Anso db73d7671a feat: RBAC, atomic deployments, and fleet-wide backups (Pro) (#181)
* feat: add RBAC viewer accounts, atomic deployments, and fleet-wide backups (Pro)

Introduces three Pro-tier features:

- RBAC: Multi-user system with admin/viewer roles, user management UI,
  automatic migration from single-admin credentials, viewer restrictions
  across the entire UI (read-only editor, hidden action buttons)

- Atomic Deployments: Pre-deploy file backup to .sencho-backup/, automatic
  rollback on health probe failure, manual rollback button, health probes
  added to stack updates, webhook-triggered deploys use atomic rollback

- Fleet-Wide Backups: Point-in-time snapshots of compose files across all
  nodes (local + remote), stored centrally in SQLite, per-stack restore
  with optional redeploy, graceful handling of offline nodes

* fix(settings): use correct ProGate prop name in UsersSection

* fix(settings): remove unused isPro prop from UsersSection

* fix(auth): fetch user info after login and setup so isAdmin is set correctly
2026-03-26 12:51:30 -04:00
Anso 4fc363301a feat(webhooks): add CI/CD webhook integration for triggering stack actions (Pro) (#177)
Add custom webhooks allowing external CI/CD systems (GitHub Actions, GitLab CI, etc.)
to trigger stack actions via HTTP POST with HMAC-SHA256 signature authentication.
Includes webhook CRUD management UI in Settings, execution history tracking,
one-time secret reveal, enable/disable toggle, and comprehensive documentation.
2026-03-26 09:23:23 -04:00
Anso 4f26f22cce feat: add Community/Pro licensing, fleet view, and UI reorganization (#145)
* feat: add license gating system with Lemon Squeezy integration

Add Community/Pro tier infrastructure:
- LicenseService singleton with Lemon Squeezy license API integration
- /api/license endpoints (GET info, POST activate/deactivate/validate)
- 14-day Pro trial activated automatically on first boot
- 72-hour periodic validation with 30-day offline grace period
- LicenseContext provider for frontend tier awareness
- License settings tab with activation UI and status display
- ProBadge and ProGate reusable components for feature gating
- requirePro per-route guard for backend Pro-only endpoints
- Proxy bypass for /api/license routes (local-only, never proxied)

* feat: add user profile dropdown and reorganize top navigation

- Create UserProfileDropdown component with settings, billing, theme
  toggle (System/Light/Dark), documentation links, and logout button
- Remove logout button from sidebar header
- Remove standalone settings button from top bar
- Move theme toggle from Settings modal to profile dropdown
- Inject app version via Vite define from root package.json
- Add globals.d.ts for __APP_VERSION__ type declaration

* refactor(settings): remove appearance tab from settings modal

Theme toggle was moved to the User Profile Dropdown in the previous
commit. Remove the now-redundant Appearance section, its nav button,
and the unused theme/setTheme props from SettingsModal.

* feat: add fleet view dashboard and about settings section

Fleet Overview: aggregates all nodes into a card grid showing status,
container counts, CPU/RAM/disk usage bars. Pro tier unlocks stack
drill-down with auto-refresh (30s). Backend endpoints /api/fleet/overview
and /api/fleet/node/:nodeId/stacks query nodes in parallel.

About section in Settings: displays version, license tier, status,
instance ID, and links to docs/changelog/issues.

Sidebar perf fix: stack status fetches now run in parallel via
Promise.allSettled instead of sequential for-loop, significantly
reducing load time for nodes with many stacks.

Also removes version number from User Profile Dropdown (now in About).

* fix(ci): resolve Docker build and E2E test failures

- Copy root package.json into frontend build stage so vite.config.ts
  can read the app version during Docker multi-stage build.
- Update auth E2E test: logout button moved into User Profile Dropdown.
- Update nodes E2E test: Settings button moved into User Profile Dropdown.
2026-03-25 08:32:07 -04:00
SaelixCode 5191737d53 feat(resources): managed/unmanaged resource separation across Resources Hub
- Classify all Docker images, volumes, and networks as managed (Sencho
  stack), external (other Compose project), or unused/system via a new
  getClassifiedResources() method and GET /api/system/resources endpoint
- Add pruneManagedOnly() + getDiskUsageClassified() to DockerController
- Prune buttons now default to Sencho-managed scope; "All Docker" is
  hidden in a ⋮ dropdown with a distinct destructive confirm dialog
- Replace Reclaimable Space donut with interactive Docker Disk Footprint
  widget (stacked bar with clickable segments that filter resource tabs)
- Add managed/external filter toggles and classification badges per tab
- GET /api/stats now returns managed + unmanaged container counts; Home
  Dashboard Active Containers card subtitle shows "N managed · N external"
- Rename "Ghost Containers" tab/copy to "Unmanaged Containers" throughout
2026-03-22 03:17:54 -04:00
SaelixCode e876a91a2e fix(lint): resolve all backend ESLint errors to pass CI lint step
Config changes (eslint.config.mjs):
- no-unused-vars: caughtErrors=none (catch clause vars are intentionally ignored throughout)
- varsIgnorePattern/argsIgnorePattern: ^_ (allow _-prefixed intentional ignores)
- ban-ts-comment, no-namespace, no-empty: downgraded to warn (pre-existing patterns)
- no-control-regex: off (terminal output processing intentionally uses control chars)

Dead code removed:
- index.ts: remove unused spawn/exec/promisify imports and dead execAsync variable
- FileSystemService.ts: remove duplicate fs default import (fsPromises already imported)
- LogFormatter.ts: remove unused parsed variable (JSON.parse used only for validation)

Auto-fixed via eslint --fix:
- prefer-const: ComposeService, DockerController, MonitorService, index.ts
2026-03-22 01:09:18 -04:00
SaelixCode 2e0f3e2711 security: harden terminal WebSocket endpoints against three attack vectors
- Reject node_proxy scoped JWT tokens with 403 on host-console and
  container exec (/ws) upgrades; machine-to-machine credentials must
  not open interactive shells
- Validate stackParam against path.resolve + startsWith(baseDir) to
  prevent directory traversal on the PTY cwd (Rule 9 pattern)
- Strip JWT_SECRET, AUTH_PASSWORD, AUTH_PASSWORD_HASH, DATABASE_URL
  from the environment passed to node-pty spawned shells
2026-03-21 00:12:16 -04:00
SaelixCode 0cb5fae947 feat(design): animated design system foundation with animate-ui and motion
Install motion + animate-ui, overhaul design tokens with brand cyan accent,
and replace CSS keyframe animations in Dialog, Tabs, Switch, and Tooltip
with spring-physics and blur-fade transitions via animate-ui Radix primitives.
2026-03-20 22:25:29 -04:00