Commit Graph

96 Commits

Author SHA1 Message Date
Anso 6f1b2cfa9c chore(main): release 0.2.2 (#98) 2026-03-24 21:28:19 -04:00
Anso 7aac99abdf chore(main): release 0.2.1 2026-03-24 19:46:44 -04:00
Anso 36a9bf3109 fix(editor): Monaco CSP fix + release pipeline fixes
fix(editor): Monaco CSP fix + release pipeline fixes — v0.2.1
2026-03-24 19:46:33 -04:00
SaelixCode 79fde6e2bd fix(editor): bundle Monaco locally to fix stuck Loading state and CSP block
- Add monaco-editor to frontend deps and configure @monaco-editor/react
  to use the local bundle via loader.config({ monaco }) instead of
  fetching from cdn.jsdelivr.net (blocked by CSP scriptSrc: self)
- Wire editorWorker via Vite ?worker syntax — blob: URLs already
  permitted by existing workerSrc CSP directive, no CSP changes needed
- fix(ci): use DOCS_REPO_TOKEN in release-please so tag creation
  cascades to docker-publish.yml (GITHUB_TOKEN cannot trigger workflows)
- fix(ci): auto-merge screenshots PR via gh pr merge --auto --squash
2026-03-24 19:43:55 -04:00
github-actions[bot] 8f7419f710 chore(main): release 0.2.0 2026-03-24 23:06:51 +00:00
SaelixCode c294def7cc feat(ci): add release-please automated versioning workflow 2026-03-24 19:01:09 -04:00
SaelixCode 6c911fd67f fix(ci): trigger docker-publish on v* tag push instead of GitHub Release event 2026-03-24 18:48:21 -04:00
SaelixCode a1b944ca14 chore(release): structure CHANGELOG for v0.1.0 2026-03-24 18:00:44 -04:00
Anso 1e0014e183 fix(docker): replace QEMU npm execution with tonistiigi/xx cross-compilation (#78)
Node.js v20 uses ARMv8.1 LSE atomic instructions that the GitHub Actions
QEMU version does not support, causing SIGILL / "Illegal instruction" core
dumps when npm ci runs inside the arm64 emulated layer.

Solution: add a dedicated prod-deps build stage that runs on $BUILDPLATFORM
(amd64, native) and uses tonistiigi/xx + xx-clang to cross-compile native
modules (bcrypt, better-sqlite3, node-pty) for the TARGET architecture
without ever executing Node.js under QEMU. The final runtime stage simply
copies the pre-compiled node_modules from prod-deps.

Stage breakdown:
  xx            - cross-compilation helper (provides xx-clang, xx-apk)
  frontend-builder  - $BUILDPLATFORM, vite build (no native modules)
  backend-builder   - $BUILDPLATFORM, tsc compilation
  prod-deps         - $BUILDPLATFORM + xx-clang cross-compiles for $TARGET
  final             - TARGET platform, no compilation at all
2026-03-24 08:29:52 -04:00
Anso 278f7f18d9 fix(ci): add linux/arm64 Docker build support with QEMU optimization (#76)
- Add docker/setup-qemu-action@v3 to docker-publish.yml (was missing,
  causing multi-platform builds to hang indefinitely)
- Add platforms: linux/amd64,linux/arm64 to build-push-action step
- Optimize Dockerfile with --platform=$BUILDPLATFORM on builder stages
  so TypeScript compilation runs at native amd64 speed; only the lean
  npm ci --omit=dev step runs under QEMU in the final stage, compiling
  the three native modules (bcrypt, better-sqlite3, node-pty) for the
  correct target architecture — reduces arm64 build time from 6+ hours
  to ~15-30 minutes
2026-03-24 07:50:33 -04:00
Anso b25574a427 feat(ui): theme-aware sidebar logo with dark/light variants (#74)
Replace single sencho-logo.png with dark and light variants that
auto-switch based on the active theme (dark/light/auto system pref).
2026-03-23 12:11:23 -04:00
SaelixCode 2e69c8ec75 chore(docs): migrate from mint.json to docs.json
Mintlify v2 requires docs.json; mint.json is deprecated and causes
"docs.json not found" errors in the Mintlify dashboard editor.

Changes:
- Add docs/docs.json with v2 schema: navigation wrapped in { groups: [] },
  theme field, logo/favicon placeholder paths at /images/logo/
- Delete docs/mint.json
2026-03-23 08:36:15 -04:00
SaelixCode 8902f6fb4c fix(ci): exclude .git from rsync --delete in sync-docs
rsync --delete removes everything in the destination that is absent from
the source. sencho/docs/ has no .git directory, so rsync was deleting
sencho-docs/.git/ on every run. The commit step then had no git repo to
work with, giving 'fatal: not in a git directory'.

Fix: add --exclude='.git' to the rsync invocation.
2026-03-23 08:02:42 -04:00
SaelixCode b24863db66 fix(ci): fix update-screenshots token and sync-docs empty-repo handling
update-screenshots:
- Add pull-requests: write to job permissions (required by
  peter-evans/create-pull-request to open a PR)
- Switch checkout and create-pull-request from GITHUB_TOKEN to
  DOCS_REPO_TOKEN (classic PAT with repo scope); GITHUB_TOKEN is
  blocked from creating PRs against protected branches

sync-docs:
- Replace actions/checkout + init fallback with a single bash step that
  runs git clone and falls back to git init on failure; actions/checkout
  on an empty repo creates a .git in a broken state (no HEAD/branch),
  which the previous [ ! -d .git ] guard never caught, leaving the
  commit step with no valid working tree
- Drop --global safe.directory config (no longer needed once we own
  the git setup ourselves)
2026-03-23 00:28:47 -04:00
SaelixCode 9505132aaf fix(ci): fix sync-docs empty-repo crash and update-screenshots protected-branch push
update-screenshots: replace direct git push to develop (blocked by branch
protection) with peter-evans/create-pull-request@v6, which opens or
updates a chore/refresh-screenshots PR instead.

sync-docs: add continue-on-error on the sencho-docs checkout so an empty
repo doesn't abort the job. A new init step runs git init -b main and
re-adds the remote when .git is missing. Final push uses
git push origin HEAD:main to work on both first-run (empty repo) and
subsequent runs (existing branch).
2026-03-23 00:09:31 -04:00
SaelixCode 932e7780ea docs: add Tier 3 reference and operations pages
- reference/settings: complete settings hub reference (all 7 tabs —
  Account, System Limits, Notifications, Appearance, Developer, Nodes,
  App Store)
- operations/troubleshooting: 1:1 path rule failures, Docker socket
  permissions, login errors, WebSocket proxy config, offline remote
  nodes, password reset, health endpoint, container logs
- operations/backup: what to back up (DATA_DIR SQLite + COMPOSE_DIR),
  hot backup via sqlite3, cron example, restore steps, host migration
  walkthrough
- mint.json: populate Reference and Operations nav groups
2026-03-22 23:25:15 -04:00
SaelixCode 7c120c671c docs: add Tier 2 feature pages
- dashboard: container stats, system stats, historical charts, docker run converter
- resources: disk footprint widget, images/volumes/networks/unmanaged tabs, prune scopes
- app-store: template grid, category filter, deploy sheet (env vars/volumes/ports), custom registry
- global-observability: unified log stream, standard vs SSE mode, filters, capacity limits
- host-console: PTY terminal, shell type, security model, common uses
- mint.json: add all 5 new pages to Features nav group
2026-03-22 22:43:53 -04:00
SaelixCode 98910c4117 docs: bootstrap user-facing documentation from codebase audit
- Add 5 new Tier 1 doc pages: configuration, stack-management, editor,
  multi-node, and alerts-notifications
- Update introduction, quickstart, and features/overview to reflect
  current feature set and link to new pages
- Restructure mint.json with Getting Started / Features / Reference /
  Operations navigation groups
- Add Playwright-captured screenshots for all major UI screens
2026-03-22 22:39:06 -04:00
SaelixCode dc79683b01 fix(ci): YAML syntax error in if condition and safe.directory for sync-docs 2026-03-22 22:00:57 -04:00
SaelixCode ed8b8e33b6 feat: add update-screenshots CI job and screenshot capture spec 2026-03-22 21:28:20 -04:00
SaelixCode 9496b14f72 feat: add automated docs pipeline and scaffold /docs folder
- Add sync-docs CI job: runs on push to main, copies /docs into sencho-docs repo via DOCS_REPO_TOKEN
- Scaffold /docs with mint.json, getting-started/introduction.mdx, getting-started/quickstart.mdx, features/overview.mdx
- Update CHANGELOG
2026-03-22 21:08:16 -04:00
SaelixCode 9f0257e94f fix(merge): resolve CHANGELOG conflict with develop 2026-03-22 19:48:37 -04:00
SaelixCode 35a57e5fa7 fix(security): disable COOP header and Vite module-preload polyfill
Two console errors on HTTP deployments with no functional impact:

1. Helmet's default Cross-Origin-Opener-Policy: same-origin is ignored
   by browsers over HTTP but logged as a console error. Disabled via
   crossOriginOpenerPolicy: false (same rationale as HSTS/COEP).

2. Vite's production build injects an inline module-preload polyfill
   script blocked by script-src 'self'. Disabled via
   build.modulePreload.polyfill: false — all modern browsers support
   link rel="modulepreload" natively.
2026-03-22 19:41:13 -04:00
SaelixCode d62ac09503 fix(stats): classify managed containers by working_dir instead of project name
Containers launched from the COMPOSE_DIR root (not individual subdirs)
all share the root folder's name as their com.docker.compose.project
label, making them appear as "external" despite being in COMPOSE_DIR.

Switch to com.docker.compose.project.working_dir: a container is
managed if its working directory is within COMPOSE_DIR, regardless
of what project name Docker Compose assigned to it.
2026-03-22 18:36:17 -04:00
SaelixCode 50df5b3c02 fix(security): explicitly disable upgrade-insecure-requests via Helmet 8 API
Helmet 8 merges custom directives with its built-in defaults, which
include upgrade-insecure-requests. Simply omitting the directive from
the custom object (PR #59) was insufficient — Helmet silently re-adds
it from defaults. Setting upgradeInsecureRequests: null is the correct
Helmet 8 API to remove a default directive.

This was the root cause of the persistent blank page on plain-HTTP
self-hosted deployments: the directive tells browsers to upgrade all
HTTP sub-resource fetches to HTTPS, producing ERR_SSL_PROTOCOL_ERROR
on every JS/CSS asset.
2026-03-22 17:26:08 -04:00
SaelixCode aeefd79b50 fix(proxy): prevent remote 401 from triggering local session logout
- Add x-sencho-proxy sentinel header to all proxied responses so
  the frontend can distinguish remote auth failures from local session
  expiry, breaking the logout loop when a node's api_token expires
- Add authMiddleware to all 5 /api/notifications endpoints that were
  missing protection (default-deny policy enforcement)
- Expand CSP to include connectSrc ws:/wss: and workerSrc blob:
  for WebSocket and Monaco editor worker support
2026-03-22 16:43:06 -04:00
Anso 5ddf85d364 Merge branch 'develop' into fix/helmet-csp-and-docker-socket-diagnosis 2026-03-22 15:51:41 -04:00
SaelixCode cf2946cfa6 fix(security): remove CSP upgrade-insecure-requests and HSTS over HTTP
Helmet's defaults are designed for HTTPS-only deployments. Two directives
were actively breaking plain-HTTP self-hosted instances:

- upgrade-insecure-requests: causes browsers to upgrade all JS/CSS/asset
  fetches to HTTPS → ERR_SSL_PROTOCOL_ERROR → completely blank page
- Strict-Transport-Security: permanently instructs browsers to refuse HTTP
  for 1 year, even after the header is removed from the server

Also handle docker socket GID=0 (root:root) edge case in entrypoint and
add [entrypoint] diagnostic log lines for easier debugging.
2026-03-22 15:42:48 -04:00
SaelixCode 593a709197 fix(docker): add entrypoint for volume permission handling
Add docker-entrypoint.sh that runs as root at startup, fixes ownership
of the DATA_DIR volume (only files with wrong user or group), then drops
to the non-root sencho user via su-exec before starting Node.

This eliminates the SQLITE_READONLY crash that occurs when a host-mounted
data volume was created by root or chowned to the wrong UID. The pattern
mirrors the official PostgreSQL, Redis, and MariaDB Docker images.

- Install su-exec in Stage 3 (10KB Alpine tool, idiomatic alternative to gosu)
- Remove USER directive; entrypoint handles the privilege drop instead
- Use ENTRYPOINT + CMD so Node becomes PID 1 (correct SIGTERM handling)
- Add .gitattributes to enforce LF line endings for *.sh files
2026-03-22 15:16:34 -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 707a5e81c1 fix(e2e): fill api_token in nodes tests so submit button is enabled
The Add Node button requires both api_url AND api_token to be non-empty
before it enables. Both validation tests were only filling api_url,
leaving the button permanently disabled and timing out after 30s.
Add a dummy api_token fill in each test so the button enables and the
form submits — the backend then correctly rejects the invalid URL.
2026-03-22 01:58:47 -04:00
SaelixCode c8a54a988b fix(lint): resolve all ESLint errors to pass CI lint step
- Replace catch (error: any) with catch (error) + (error as Error).message cast
  in EditorLayout, NodeManager, HomeDashboard, AppStoreView
- Define TemplateVolume interface in AppStoreView; replace volumes any[] with typed array
- Define MetricPoint interface in HomeDashboard; replace metrics any[] with MetricPoint[]
- Define TerminalContainer type in BashExecModal; replace as any DOM property casts
- Define NodeTestInfo interface in NodeManager; replace info: any with typed shape
- Fix DockerNetworkStats cast in EditorLayout container stats WebSocket handler
- Remove unused catch variable (e) in api.ts and other components
- Cast streamFilter onValueChange val to union type in GlobalObservabilityView
- Add eslint-disable-next-line react-refresh/only-export-components to badge.tsx,
  button.tsx, AuthContext, NodeContext, use-data-state, use-is-in-view
- Add eslint-disable-next-line react-hooks/set-state-in-effect in LogViewer
- Add /* eslint-disable */ to animate-ui third-party primitive files
2026-03-22 00:27:43 -04:00
SaelixCode f7471a1a18 fix(e2e): get all E2E tests passing and fix AlertDialog crash on delete
- Fix rate limiter to allow 100 attempts in dev mode so E2E tests are
  not blocked by failed-login attempts during test development
- Simplify logout button selector to use Lucide icon class (lucide-log-out)
  instead of the fragile Tooltip-content locator chain that broke on navigation
- Rewrite stacks E2E spec: use waitForFunction to wait for sidebar to load,
  delete leftover stacks via browser-context fetch before creating, and use
  page.reload() to get a clean sidebar state
- Fix AlertDialogContent: remove asChild+motion.div pattern that triggered a
  React.Children.only crash — Radix AlertDialog.Content injects a second
  DescriptionWarning child internally, breaking Slot when asChild=true; replace
  with CSS keyframe animations (data-[state=open]:animate-in)
- Fix final assertion in delete test to use exact text + listbox scope to avoid
  false positives from similarly-named stacks like e2e-test-stack-*
- All 6 E2E tests pass (4 auth + 2 stacks); node tests skip gracefully
2026-03-21 23:58:46 -04:00
SaelixCode ce50db0fde security: pre-release hardening, automated testing, and production readiness
SECURITY (critical fixes):
- Add authMiddleware to /api/system/console-token (was publicly accessible)
- Validate api_url on node create/update to prevent SSRF (rejects localhost/loopback)
- Add rate limiting (5 req/15 min/IP) to /api/auth/login and /api/auth/setup
- Fix path traversal in env_file resolution — absolute/escaping paths rejected
- Add stack name validation to GET routes (was only on PUT/POST)
- Add helmet security headers middleware
- Restrict CORS to FRONTEND_URL in production

PRODUCTION READINESS:
- Add GET /api/health public endpoint + HEALTHCHECK in Dockerfile
- Add SIGTERM/SIGINT graceful shutdown handler (drains connections, closes DB)
- Run container as non-root sencho user in Dockerfile

QUALITY:
- Fix 4 silent empty catch{} blocks in EditorLayout (now show toast.error)
- Connect ErrorBoundary to root App in main.tsx
- Replace WebSocket.Server with named WebSocketServer import (ESM compat)

TESTING (new automated test suite):
- Install Vitest; 38 backend tests across 4 suites covering validation utilities,
  health endpoint, auth middleware, login flows, SSRF protection, and path traversal
- Extract isValidStackName/isValidRemoteUrl/isPathWithinBase to utils/validation.ts
- Playwright E2E scaffolding: auth, stacks, nodes specs + shared login helper
- CI: run Vitest + ESLint on every PR
2026-03-21 21:59:44 -04:00
SaelixCode 1690f0d3c6 feat(notifications): aggregate alerts from all nodes in the notification panel
Remote-node alerts now appear in the local notification bell alongside
local ones, each tagged with the originating node name.

- backend: reorder WS upgrade handler so /ws/notifications?nodeId=<remote>
  falls through to the existing proxy path instead of short-circuiting
- frontend/api.ts: add fetchForNode() helper for explicit node-targeted
  requests without touching the localStorage active-node key
- frontend/EditorLayout: fetch notification history from all registered
  nodes in parallel on mount and on node-list changes; open a per-remote
  WebSocket connection for real-time push; route mark-read / delete / clear
  actions back to the originating node; show node-name badge on remote alerts
2026-03-21 17:38:55 -04:00
SaelixCode 30fe77cd5d fix(ws): fix remote node console by delegating console session tokens
When the gateway proxied a WebSocket upgrade for a remote node's interactive
terminal (host console or container exec), it injected the long-lived api_token
(scope: 'node_proxy') as the Bearer token. The remote's WS upgrade handler
correctly blocked this token via its isProxyToken guard (added in a prior
security hardening commit), causing a 403 → socket destroy → "connection error,
session ended" on the client.

Fix: introduce a POST /api/system/console-token endpoint that issues a
short-lived JWT (scope: 'console_session', 60 s TTL). Before forwarding an
interactive WS upgrade to a remote node, the gateway calls this endpoint using
the api_token, then substitutes the returned console_session token as the Bearer
header for the WS upgrade. The remote's isProxyToken guard (scope === 'node_proxy')
continues to block the long-lived api_token from spawning shells directly while
allowing gateway-delegated console sessions through.

Non-interactive WS paths (stack logs) continue to use the api_token unchanged.
2026-03-21 16:53:46 -04:00
SaelixCode ed6954307b fix(proxy): skip express.json() for remote proxy requests to fix body forwarding
The previous fix attempted to re-stream req.body via proxyReq.write() in the
proxyReq event handler. This raced against http-proxy's synchronous
req.pipe(proxyReq) call: when express.json() consumed the IncomingMessage stream,
Node.js's pipe() detected readableEnded and scheduled process.nextTick(proxyReq.end),
which fired before the proxyReq socket event. Any subsequent write() threw
"write after end", leaving the remote server stalled waiting for body bytes that
never arrived (the "Add Rule spins forever" bug).

Root fix: a conditional middleware now skips express.json() entirely for requests
targeting a remote node on non-auth, non-nodes API paths. The raw stream is left
intact, allowing http-proxy's req.pipe(proxyReq) to forward the body correctly.
2026-03-21 11:56:24 -04:00
SaelixCode a703707aa0 fix(proxy): re-stream express.json()-consumed body to remote nodes for POST/PUT/PATCH
express.json() fully drains the incoming request stream. http-proxy-middleware then
pipes an already-consumed empty stream to the remote Sencho instance, which holds the
connection open waiting for body bytes that never arrive (Content-Length says N but 0
bytes are forwarded). This caused all POST/PUT/PATCH requests to remote nodes — most
visibly 'Add Alert Rule' — to hang indefinitely with an infinite loading spinner.

Fix: in the proxyReq handler, write JSON.stringify(req.body) with correct Content-Type
and Content-Length headers before forwarding, restoring the full request body.
2026-03-21 01:26:12 -04:00
SaelixCode e190f3ad8a fix(alerts): overhaul alerts & notifications system for local and remote nodes
- StackAlertSheet: fetch agent status on open and show contextual banner
  (amber warning when no channels configured, green when active, blue info
  callout on remote nodes explaining remote-instance evaluation semantics)
- StackAlertSheet: surface actual server error message on addAlert failure
  instead of generic string; add console logging for all failure paths
- SettingsModal: expose Notifications tab for remote nodes so agents can be
  configured directly on any Sencho instance; section header shows node name
  and Remote badge with tooltip when proxying to a remote node
- SettingsModal: re-fetch agents/settings when active node changes
- SettingsModal: add hidden DialogTitle/DialogDescription to satisfy Radix
  UI accessibility requirements (eliminates console errors)
- backend POST /api/alerts: add Zod validation schema; rejects unknown
  metric/operator values, negative thresholds, and missing fields with 400
- EditorLayout WS: upgrade reconnect from flat 5s retry to exponential
  backoff (1s→2s→4s→8s→16s→30s max); onerror now logs the event; cleanup
  only closes OPEN sockets — CONNECTING sockets are closed in onopen after
  isMounted check, eliminating "closed before established" StrictMode error
2026-03-21 01:19:29 -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 22e646286e fix(ui): resolve 9 animated design system bugs including Monaco tab height accumulation
- fix(editor): Monaco compose↔.env tab switching no longer accumulates height
  each switch — root cause was Monaco's position:static internal child creating
  a circular CSS dependency with the CSS grid; broken by calling layout({0,0})
  to force reflow before re-measuring. Added overflow-hidden to Monaco container.
- fix(ui): sliding tab indicator on compose/env tabs via motion.div layoutId
- fix(ui): sliding tab indicator on Notification tabs (Discord/Slack/Webhook)
- fix(ui): switch thumb now animates position (Radix data-attribute CSS translation)
- fix(ui): 'Always Local' tooltip no longer crashes — replaced animate-ui tooltip
  (getStrictContext throws outside provider) with pure Radix primitives + CSS animation
- feat(settings): Auto theme option added (light/dark/auto with matchMedia listener)
- fix(ui): NodeManager dialog buttons no longer stuck together (DialogFooter gap)
- fix(ui): AlertDialog spring animation + Quick Clean button text wraps correctly
- fix(ui): Resources/App Store/Logs buttons toggle off on second click
2026-03-20 23:51:54 -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
SaelixCode a5ac3e4981 feat(notifications): replace polling with WebSocket push
Eliminates the 5-second setInterval polling loop for notifications in
EditorLayout. A persistent /ws/notifications WebSocket connection is
opened on mount; the backend pushes each alert the instant dispatchAlert
fires, with 5s auto-reconnect on close.

Key changes:
- NotificationService: injectable broadcaster callback (setBroadcaster)
- DatabaseService.addNotificationHistory: returns full NotificationHistory
  record (with id/is_read) instead of void
- index.ts: notificationSubscribers Set + /ws/notifications upgrade handler
  (JWT-verified, placed before remote proxy path)
- EditorLayout: polling removed, WS connect/reconnect replaces it
2026-03-20 20:47:51 -04:00
SaelixCode f7e8e40915 feat(settings): scope split — developer settings always target local node
- Add localOnly option to apiFetch — omits x-node-id header so the
  request bypasses the proxy and always hits the local Sencho instance
- fetchSettings now performs two fetches when a remote node is active:
  active node fetch for per-node settings (CPU/RAM/disk limits, janitor,
  crash detection), and a localOnly fetch for UI preferences
  (developer_mode, global_logs_refresh, metrics_retention_hours,
  log_retention_days)
- saveDeveloperSettings passes localOnly: true — developer preferences
  can no longer be written into a remote node's database
- Update scope badges: System Limits shows "Configuring: [node]",
  Developer shows "Always Local" when a remote node is active
2026-03-20 20:12:00 -04:00
SaelixCode 322e717514 feat(settings): harden settings API and overhaul SettingsModal
Security:
- Strip auth credential keys (auth_username, auth_password_hash,
  auth_jwt_secret) from GET /api/settings response
- Add allowlist guard to POST /api/settings — rejects unknown or
  auth-namespace keys with a 400

Backend:
- Add PATCH /api/settings bulk endpoint with Zod schema validation
  (type coercion, range checks, URL format) and atomic SQLite transaction
- Add system_state table — moves last_janitor_alert_timestamp out of
  global_settings; adds getSystemState/setSystemState on DatabaseService
- Add metrics_retention_hours and log_retention_days configurable settings;
  MonitorService reads both dynamically each evaluation cycle
- Add cleanupOldNotifications(days) to DatabaseService, called each cycle

Frontend:
- Replace single isLoading flag with per-operation states
  (isSavingSystem, isSavingDeveloper, isSavingPassword, isSavingRegistry,
  isSavingAgent/isTestingAgent per agent type)
- Add skeleton loader that blocks interaction until fetchSettings resolves
- Explicit key-picking in fetchSettings — auth keys cannot enter state
- Unsaved-changes amber dot on System Limits and Developer sidebar items
- Separate saveSystemSettings / saveDeveloperSettings — no cross-tab clobber
- Developer tab gains Data Retention section (metrics hours, log days)
- All settings saves use new PATCH /api/settings endpoint
2026-03-20 19:57:34 -04:00
Anso 5c62a4a630 Merge branch 'develop' into feature/app-store-categories 2026-03-20 15:51:28 -04:00
SaelixCode 34cad76d45 feat(app-store): category filter bar and custom registry settings
- TemplateService: static LSIO_CATEGORY_MAP covers ~80 well-known apps
  across Automation, Downloaders, Media, Monitoring, Networking,
  Security, Development, Productivity, Utilities, and Other. Category
  lookup is O(1) and runs once at cache-fill time. Adds source field
  ('linuxserver' | 'custom') to Template for future per-source UX.
  Portainer v2 registries pass their native categories through unchanged.
  Adds clearCache() method wired to POST /api/templates/refresh-cache.

- AppStoreView: category pill bar (All + sorted dynamic categories)
  rendered between search and grid; active pill fills on click; clicking
  a category badge on a card also sets the active filter; app count
  updates reactively; filter composed with existing search query.

- SettingsModal: new App Store section (local-node only) exposes a
  custom Portainer v2 JSON URL input with Save & Refresh (saves setting
  then busts the 24h template cache) and Reset to Default.
2026-03-20 15:19:32 -04:00
SaelixCode 74964b0e26 fix(stats): throttle container stat WebSocket updates via ref buffer
Each container's onmessage handler was calling setContainerStats()
independently, causing React to schedule up to N separate reconciliation
passes per second (one per container). With 20 containers streaming Docker
stats at ~1 update/s, EditorLayout was re-rendering up to 20 times/s.

Fix: incoming stats are written into pendingStatsRef (no re-render cost),
then flushed to React state in one batched setContainerStats call every 1.5s.

Two bugs in the original proposal are addressed:
- rawBytesRef (never cleared) owns rx/tx tracking so net I/O rate is always
  accurate; avoids the stale containerStats closure that would have shown
  0 B/s after every flush cycle
- pending snapshot is captured and cleared BEFORE calling setState so the
  functional updater stays pure (no side-effects inside it)
- pendingStatsRef is cleared in the effect cleanup so stale entries from
  the previous stack don't briefly appear on stack switch
2026-03-20 12:14:09 -04:00
Anso 36cc14bac7 Merge branch 'develop' into fix/logs-view-oom-virtualization 2026-03-20 12:08:01 -04:00
SaelixCode 753b0c3539 fix(logs): use monotonic _id key to prevent O(n) DOM mutations on scroll
key={idx} with a shifting .slice(-300) window meant every new log arrival
caused React to update all 300 existing DOM nodes (index 1 became 0, 2
became 1, etc.), even though only one entry actually changed.

The proposed content-hash fix (timestamp + containerName + message.substring)
carries a real collision risk: chatty containers emitting identical lines at
the same millisecond produce duplicate keys, triggering undefined React
reconciliation behaviour.

Fix: add a _id: number field to LogEntry, stamped client-side with a
monotonic counter (logIdRef) at the point of ingestion — once in the SSE
onmessage handler and once in the polling setLogs path. React now tracks
data identity rather than position, so the slice window can shift without
touching the 299 unchanged DOM nodes.
2026-03-20 12:06:36 -04:00