Commit Graph

249 Commits

Author SHA1 Message Date
Anso 7f17ada531 chore(main): release 0.42.0 (#448) 2026-04-08 20:14:26 -04:00
Anso 3ee4fe6e44 feat(topology): overhaul network topology with dagre layout, enriched nodes, and click-to-logs (#447)
Replace N+1 Docker API calls (inspectNetwork per network) with a
container-centric approach that fetches all networks and containers
in 2 parallel calls, then maps relationships in memory.

Add dagre auto-layout algorithm for hierarchical DAG visualization,
replacing the static two-row layout that caused edge spaghetti at scale.

Add "Show system networks" toggle, enrich container nodes with running
state indicators, stack badges, and base image names. Clicking a
running container opens its log viewer directly from the topology graph.
2026-04-08 20:05:22 -04:00
Anso ea03406c6b chore(main): release 0.41.2 (#445) 2026-04-08 17:13:15 -04:00
Anso 8080540881 fix(fleet): strip trailing slash in fetchRemoteMeta URL construction (#444)
Nodes with a trailing slash in api_url (e.g. http://host:3000/) caused
fetchRemoteMeta to construct a double-slash URL (http://host:3000//api/meta),
which failed silently. The update-status endpoint then returned version: null
for the remote node, triggering a false "Update available" badge.

Every other URL construction in the codebase already strips the trailing
slash; this was the one call site that was missed.
2026-04-08 17:08:39 -04:00
Anso d632a6a207 chore(main): release 0.41.1 (#442) 2026-04-08 15:05:02 -04:00
Anso 6fff2c2d35 fix(fleet): resolve self-update compose file access and improve completion detection (#441)
The self-update feature failed on remote nodes because SelfUpdateService
ran `docker compose -f <host_path>` inside the container, where the host
compose file path does not exist. The fix splits the update into two
steps: (1) pull the latest image directly via `docker pull`, and (2)
spawn a short-lived helper container that mounts the compose directory
from the host and runs `docker compose up --force-recreate`.

Additional changes:
- Use execFileSync/execFile with argument arrays instead of shell strings
  to eliminate shell injection surface from Docker label values
- Add Signal 4 completion detection: mark update as completed when the
  remote version matches the gateway version (with 15s elapsed guard)
- Extend early failure heuristic from 90s to 3 minutes for slow pulls
- Distinguish "node unreachable" from "node lacks self-update capability"
  in error messages; use silent skip in update-all to avoid res crashes
- Add requireAdmin guard to POST /api/system/update
- Handle comma-separated compose config file paths (multiple -f flags)
- Update fleet docs with self-update mechanism, troubleshooting entries
2026-04-08 14:59:11 -04:00
Anso f41f3a1639 chore(main): release 0.41.0 (#429) 2026-04-08 10:55:38 -04:00
Anso 8e0857e7f6 feat(contact): add official contact emails throughout app and docs (#428)
Surface six dedicated email channels (support, contact, licensing,
security, privacy, conduct) across contextually appropriate locations:

- security.txt: create .well-known/security.txt for app (RFC 9116)
- SECURITY.md: add security@sencho.io as alternative to GitHub reporting
- PaidGate/AdmiralGate: add licensing@sencho.io to upgrade prompts
- docs: new Contact & Support reference page listing all channels
- CHANGELOG: document additions under [Unreleased]

Website changes (separate repo): privacy@sencho.io in Privacy/Terms
pages, licensing@sencho.io in Refund page and pricing footer,
contact@sencho.io in website footer.
2026-04-08 09:52:42 -04:00
Anso be7eda85f1 fix(billing): hide billing portal for lifetime licenses (#427)
Lifetime licenses have no recurring subscription, so the Lemon Squeezy
customer portal cannot generate a URL. The Manage Subscription button in
Settings already had the isLifetime guard, but the Billing button in the
profile dropdown did not, causing a confusing "No billing portal
available" error.

- Add !license.isLifetime guard to UserProfileDropdown (matches
  LicenseSection pattern)
- Move lifetime detection into getBillingPortalUrl() so the service owns
  all billing eligibility logic
- Change return type to { url } | { error } discriminated union for
  clear error propagation
2026-04-08 09:51:27 -04:00
Anso f6d2199978 feat(resources): add loading toast for prune, delete, and purge operations (#426)
Show a loading notification with spinner and indeterminate progress bar
while Resource Hub operations are in progress, replacing the dead moment
between confirmation and result.
2026-04-08 09:46:27 -04:00
Anso 662bc1a210 fix(resources): unify container/resource classification with multi-fallback resolution (#425)
* fix(sidebar): add service name and config_files fallbacks for container-to-stack matching

Containers that predate Sencho's reorganization of compose files into
subdirectories carry stale Docker labels where the project name is set
to the COMPOSE_DIR basename (e.g. "compose") rather than the stack
directory name. The existing project name map and working_dir fallbacks
from PR #416 did not cover this case.

Added two new fallback strategies to getBulkStackStatuses:
- Match by com.docker.compose.service label against known stack names
- Extract stack name from com.docker.compose.project.config_files path

Also reused the existing isPathWithinBase utility for path containment
checks and hoisted path.resolve(COMPOSE_DIR) out of the per-container
loop.

* fix(resources): unify container/resource classification with multi-fallback resolution

Extract shared helpers (resolveContainerStack, resolveProjectLabel,
buildAbsDirMap) and apply them consistently across getClassifiedResources,
pruneManagedOnly, getDiskUsageClassified, and getBulkStackStatuses.

Fixes incorrect "External" tagging in Resources Hub for images, volumes,
and networks belonging to stacks that predate Sencho's compose file
reorganization. Also fixes the "active" plural on the dashboard
Containers card.
2026-04-08 09:46:21 -04:00
Anso 2354beed02 fix(console): send proxy tier headers for remote node console-token requests (#424)
The gateway's console-token fetch to remote nodes was missing the
x-sencho-tier and x-sencho-variant headers. Without them, the remote's
requireAdmiral guard fell back to its own local license (Community),
rejecting the request with 403 and surfacing as a 502 "Connection error"
in the browser.

Additionally, the remote's WS upgrade handler for host-console checked
only the local LicenseService instead of respecting proxy tier headers
on console_session tokens. Both paths now propagate and honour proxy
tier headers consistently.
2026-04-08 09:46:14 -04:00
Anso a6849aedab feat(dashboard): add node badge to Recent Alerts for remote node alerts (#423)
Alerts from remote nodes now display a node name badge in the Recent
Alerts card, matching the existing badge style in the notification
panel. This helps users identify which node generated each alert.
2026-04-08 09:46:08 -04:00
Anso ce6d435dc1 chore(main): release 0.40.0 (#421) 2026-04-07 01:54:15 -04:00
Anso 455bfa8734 fix(schedules): filter auto-update policies from Scheduled Operations view (#420)
* fix(schedules): filter auto-update policies from Scheduled Operations view

Auto-update policies (action=update) were appearing in both the
Auto-Update tab and the Scheduled Operations tab. Added server-side
action/exclude_action query params to GET /api/scheduled-tasks.
ScheduledOperationsView now requests ?exclude_action=update and
AutoUpdatePoliciesView requests ?action=update, so each view only
shows its relevant tasks.

* fix(schedules): use typeof guard for query param type safety

Express can parse ?action[]=x as an array. Use typeof === 'string'
guard instead of unsafe type assertion.
2026-04-07 01:49:22 -04:00
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 6ba4703e1e chore(main): release 0.39.6 (#414) 2026-04-06 23:13:35 -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 e9fb80869b chore(main): release 0.39.5 (#411) 2026-04-06 22:11:25 -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 f9d4756b7e chore(main): release 0.39.4 (#408) 2026-04-06 20:27:40 -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 9178da6fa6 chore(main): release 0.39.3 (#403) 2026-04-06 03:08:23 -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 74962c5810 chore(main): release 0.39.2 (#400) 2026-04-06 01:50:14 -04:00
Anso 2089e75ef1 fix(fleet): filter invalid version strings from UI display (#399)
Treat 'unknown' and '0.0.0-dev' as absent versions in the node
switcher, fleet overview, and capability gate. Remote nodes running
older Docker images that return these fallback values now show a
clean label instead of "vunknown".
2026-04-06 01:43:52 -04:00
Anso d7babfb0a5 chore(main): release 0.39.1 (#397) 2026-04-06 00:48:38 -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 34c3b5d80f chore(main): release 0.39.0 (#393) 2026-04-05 23:43:56 -04:00
Anso bbd51387ba chore(main): release 0.38.6 (#392) 2026-04-05 23:39:15 -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 5c41c9a012 chore(main): release 0.38.5 (#389) 2026-04-05 22:53:00 -04:00
Anso dee7c6685b fix(fleet): resolve remote node capability detection failures (#388)
* fix(licensing): backward-compatible tier/variant enforcement and self-healing variant detection

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

* fix(fleet): resolve remote node capability detection failures

Exempt /api/meta and /api/health from the global rate limiter so
capability fetches are never blocked by proxied traffic. Add
backend-side caching (3-min TTL) with stale-while-revalidate to
absorb transient failures. Shorten frontend failure cache to 30s
for faster recovery. Evict meta cache on node deletion.
2026-04-05 22:46:47 -04:00
Anso 23a4c99087 chore(main): release 0.38.4 (#386) 2026-04-05 21:55:14 -04:00
Anso 591440f21a chore(main): release 0.38.3 (#383) 2026-04-05 20:03:59 -04:00
Anso 1dd87ec887 chore(main): release 0.38.2 (#380) 2026-04-05 18:50:45 -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 969fa7ab86 chore(main): release 0.38.1 (#377) 2026-04-05 17:31:38 -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 850293238e chore(main): release 0.38.0 (#372) 2026-04-04 02:55:56 -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 bf242e8fd7 chore(main): release 0.37.0 (#369) 2026-04-03 21:55:17 -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 ebf71dee7a chore(main): release 0.36.0 (#366) 2026-04-03 20:40:40 -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 184e736aa6 chore(main): release 0.35.0 (#363) 2026-04-03 17:35:31 -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 c5650afa95 chore(main): release 0.34.0 (#360) 2026-04-03 11:37:57 -04:00