3591 Commits

Author SHA1 Message Date
claude code agent 400ac05864 fix(#32): webhook URL via shared helper, disabled-state note, CAS-release test, Conflict helper (review round 1)
F1 [WARNING] The frontend built the trigger URL from window.location.origin,
bypassing the shared webhookHelper whose getBaseUrl() honours the reverse-proxy
sub-path (<base href>) and the desktop file:// build. Under a non-root base-href
deploy the copied URL would miss the sub-path prefix. Added
containerAutomationWebhookUrl(token) to webhookHelper (mirroring dockerWebhookUrl)
and used it; the test asserts against the same helper.

F2 [WARNING] The panel showed the live URL + 'POST runs a pass' text even when
auto-update was disabled — contradicting the server, which 409s a valid token
while disabled. WebhookTriggerSection now takes the enabled flag and shows an
orange note ('trigger is inactive... a POST returns 409') when a token exists but
auto-update is off; the token/Regenerate/Clear stay (the token is kept). Tests
cover both the disabled note and its absence when enabled.

F3 [WARNING] The CAS-release of the REAL runUpdatePass was untested (only the
acquire-FAIL path was). A broken release defer would wedge auto-update forever
(poll drops every tick, webhook worker spins) with a green suite. Added
TestRunUpdatePassReleasesLock: a test store with auto-update enabled and no
endpoints runs a pass to completion and asserts the lock is released (and a
second pass re-acquires it).

F4 [low] Swapped raw NewError(StatusConflict) for the httperror.Conflict helper,
matching the rest of the file and webhook_create.go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:30:38 +03:00
claude code agent b6ba9b46c7 feat(automation): inbound registry-push webhook for immediate auto-update (#32)
Adds POST /api/webhooks/container-automation/{token} which kicks a full
auto-update pass immediately, instead of waiting up to PollInterval (6h) for the
poll. Registry-agnostic 'general kick': the payload is not parsed; the pass
compares digests and updates only stale containers. Polling stays as a fallback.

- Service.TriggerUpdate(): a buffer-1 non-blocking kick channel + a worker
  goroutine on baseCtx that debounces ~10s (collapsing a multi-arch/multi-tag
  push burst into one pass). update() is split into runUpdatePass() bool, which
  returns false when the updateRunning CAS is not acquired; the worker backs off
  and re-runs so a kick landing during a scheduled pass is never lost. The poll
  timer keeps calling the same pass.
- Worker panics are recovered (poll parity — the poll path runs under the
  scheduler's cron.Recover), so a panic in the pass cannot crash the daemon from
  this bare goroutine. runUpdatePass also re-checks AutoUpdate.Enabled as
  defense-in-depth against a disable during the debounce window.
- Route via bouncer.PublicAccess (distinct from /webhooks/{token} by segment
  count); token compared with subtle.ConstantTimeCompare; empty/mismatched token
  -> 404, disabled auto-update -> 409, success -> 202. Token is server-generated
  (uuid) via RegenerateWebhookToken / ClearWebhookToken settings actions; never
  accepted from the client; excluded from /settings/public (present in admin GET
  for the UI).
- Frontend: an AutoUpdatePanel 'Update trigger webhook' section (readonly URL,
  copy, Generate/Regenerate, Clear, registry-setup hint).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:05:21 +03:00
vvzvlad 5f6e344fe5 Merge pull request 'fix(automation): update a single container instead of redeploying its stack' (#28) from fix/container-update-recreate-single into develop
Reviewed-on: #28
2026-07-02 23:24:13 +03:00
agent_coder bb68acfbf6 fix(#29 review r1): stack-delete leak, rollback/retention/version tests, UX trap
F3: deleting a file-based stack now removes the stack ROOT (compose/{id}) via a
new removeStackProjectDir helper, not stack.ProjectPath (which the PR repointed to
compose/{id}/v{N}) — old version dirs + parent no longer leak. Git stacks unchanged.
F1: tests for validateRollbackTarget (rejects 0/neg/>current/hole) and the rollback
snapshot (client content ignored, target read from disk, monotonic new version, note).
F2: tests for pruneStackFileVersionDirs (deletes given dirs, swallows errors) + the
post-commit gate contract + a monotonic-version regression guard.
F4: handler tests for ?version= (negative/out-of-range -> 400, valid version served,
legacy fallback).
F5: swagger @param version on GET file; @version 2.44.0 (handler.go) + package.json
2.44.0, matching APIVersion.
F6: the version selector no longer sets rollbackTo for the current/top version and
clears it on a manual buffer edit (so edits are honored, not silently discarded);
returning to the current version restores the current content. Distinguishes real
user edits from the programmatic version-load (CodeMirror ExternalChange).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:28:52 +03:00
agent_coder d0d3c068ba feat(stacks): file-based stack versioning with full history + rollback (#27)
Adds append-only version history on disk (compose/{id}/v{N}/<files>) for
file-based (WorkflowID==0) Compose/Swarm stacks, with rollback to any past
version. Git stacks (versioned by commit) and Kubernetes are untouched.

Backend:
- Stack model: StackFileVersion, PreviousDeploymentInfo, Versions[]; new
  StackFileVersionInfo type. APIVersion 2.43.0 -> 2.44.0.
- Versioned multi-file snapshot (entrypoint + AdditionalFiles) into v{N}/;
  ProjectPath repointed via GetStackProjectPathByVersion each deploy. Retention
  cap (20): Versions[] trimmed in-tx, old dirs deleted only AFTER the tx commits.
- Update handlers: RollbackTo (content read server-side from the target version,
  never trusted from the client; validated 1..current & present in Versions).
- Create paths seed v1. stackFile reads ?version= (validated; negative -> 400).
- New GET /stacks/{id}/versions endpoint.
- Migration 2.44.0: move existing file-based stacks' files into v1/ (idempotent,
  atomic pre-read of the full file set, skips git/kube/orphans).

Frontend:
- useStackVersions query + stackVersions key; StackEditorTab builds the full
  history list; StackVersionSelector shows 'v{N} · date · author'; file/versions
  caches invalidated (by prefix) after deploy/rollback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:07:26 +03:00
vvzvlad e63d2ffe9b fix(automation): update a single container instead of redeploying its stack
Clicking "Update" on a stack member (and the native auto-update daemon
updating one) redeployed the whole compose stack instead of updating just
that container. Match Watchtower behaviour: always recreate the single
container with a re-pull. The recreate endpoint preserves config + compose
labels, so the container stays part of its project.

Collapse all update surfaces to a single-container recreate and drop the
now-dead stack-aware routing:
- frontend: "Update now" button, list badge and bulk "Update selected" now
  recreate each container individually; remove standalone/stack/external
  routing, the external refusal, the PortainerStackUpdate gate and the
  stack-update confirm dialog.
- daemon: route every outdated candidate through updateStandalone; remove
  updateStack, the stack/external grouping and the stackDeployer dependency.
- add a regression test asserting a Portainer-managed compose-stack member is
  recreated individually, not stack-redeployed.

Behavioural notes: git/external compose containers are now auto-updated too
(were detect-only), and updating a stack member no longer requires
PortainerStackUpdate (same auth as the normal Recreate action).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 15:21:29 +03:00
claude code agent 5a4d7e557b Merge branch 'tmp/issue17' into fix/portainer-issues 2026-07-01 23:46:09 +03:00
agent_coder 3c7400ca2a fix(update): derive git-stack flag from WorkflowID to match Go daemon (closes #17)
The TS update-routing used the deprecated `!!stack.GitConfig` to flag a
git-backed stack, which can diverge from the canonical Go daemon routing
(`IsGit: st.WorkflowID != 0`) on the new Workflow/Source model. Derive it from
WorkflowID instead (added WorkflowID to the client Stack type). The stack-type
filter (Type === DockerCompose) was already in place and tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:34:10 +03:00
agent_coder cdb1f8648a fix(auth): drop dead SERVER_TYPES.LDAP branch in isEditLDAP (closes #21)
SERVER_TYPES only has CUSTOM and AD (OpenLDAP was retired in #5), so the
'|| ServerType === SERVER_TYPES.LDAP' clause referenced a non-existent key and
was always false. AD is the only edit-LDAP server type now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:30:09 +03:00
vvzvlad 594312a777 Merge pull request 'feat(automation): native container auto-update (Watchtower-style) + auto-heal (#3)' (#19) from feat/3-auto-update into develop
Reviewed-on: #19
2026-07-01 23:25:24 +03:00
agent_coder 492d3d01b0 feat(#19): separate webhook per automation mechanism (update vs heal)
Split the single container-automation webhook URL into two independently
optional URLs — UpdateWebhookURL (fired on update/rollback/update-failed) and
HealWebhookURL (fired on auto-heal restart). The notifier routes each event to
its mechanism's URL by kind; an empty URL silences only that mechanism, so a
user can enable notifications for updates without heal (or vice-versa).

Settings gain both fields (each validated http/https, {{message}} allowed), the
NotificationPanel exposes two labeled inputs, and the golden migration output is
updated. Delivery path (goroutine/recover/timeout, {{message}} GET vs POST,
per-container stack message format) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:47:25 +03:00
vvzvlad 6a4242b166 Merge pull request 'feat(logs): live HTTP stream + append-only render (#2)' (#6) from feat/2-stream-logs into develop
Reviewed-on: #6
2026-07-01 22:09:47 +03:00
agent_coder 8829c8cfd2 fix(#6): theme-consistent search highlight + Copied ack on Copy button
- F3: give the <mark> search highlight the full theme-token tier set
  (light / th-dark / th-highcontrast, with a legible text colour) so it reads
  correctly in every theme now that the viewer is theme-aware.
- F6: restore the transient 'Copied' acknowledgement on the Copy button via
  useCopy's copiedSuccessfully (Check icon + 'Copied' label).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:46:55 +03:00
agent_coder 331319f7f1 feat(logs): theme-aware log viewer (standard colors), fill height
Per maintainer feedback ('сделай поддержку светлой/темной темы', 'цвета фона
карточки подтяни к стандартным', and the white gap below):
- Drop the hardcoded dark palette; style the card/header/toolbar with the
  project's standard theme tokens (Card/Card.Header token set) + th-dark: /
  th-highcontrast: variants, so it adapts to light/dark/high-contrast like every
  other Portainer view. Reuse project Button/Input/Checkbox/Icon components; the
  log body uses the themed .log_viewer class; the range picker inherits themed
  form-control styling.
- Fill the available page height (root flex column, calc(100vh - nav/header),
  log body flex:1 min-h:0 overflow-y) so there's no dead white space below.
Layout (single toolbar row, three toggles, gutter), real data, and props are
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:35:49 +03:00
agent_coder a60b7be55d fix(#6): address review — hook reconnect/trim tests, sticky error banner, stale comment
- F1: cover the hook's riskiest path — a following stream that ends with an
  unwritten tail fragment then resumes (tail:0 + nano-since), asserting the
  fragment is dropped, resume params are correct, and the boundary line is
  deduped to one; plus MAX_LOG_LINES head-trim and buffer reset on
  resourceId/lineCount change.
- F2: clear the error banner on a SUCCESSFUL reconnect (via a new onOpen signal
  on StreamLogsFn), not only when new lines arrive — an idle-but-healthy
  reconnect no longer leaves a stuck 'unable to stream' banner.
- F4: update the stale comment in the React logs view registration (the React
  logs migration is now complete).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:55:15 +03:00
agent_coder 5bb678d3ba fix(#19): address review F1-F4 (badge test, force write-back test, force throttle, per-container stack notifications)
- F1: test that clicking the badge/UpdateNowButton actually dispatches the update
  (confirm->mutate) for standalone and stack, and not on dismiss.
- F2: Go test that a successful forced re-check repopulates the caches (a later
  non-force read hits cache, no second registry HEAD).
- F3: throttle forced image-status re-checks against registry amplification —
  coalesce concurrent forced re-checks of the same image via singleflight, plus a
  5s per-image min-interval (== remoteDigestCache TTL) caching only successes. The
  non-force path (daemon + background badges) is unchanged.
- F4: notifications are now per-container. Stack-member containers each emit their
  own EventUpdated (not one aggregate stack event), Event carries the stack name
  (from the com.docker.compose.project label), and the new image digest is fetched
  best-effort by re-inspecting the container after the redeploy. Message:
  'Environment | .. / Stack [<name>] / Update [<container>]: <old> -> <new>'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:50:55 +03:00
agent_coder f6752130ac feat(logs): re-skin viewer to maintainer's mockup design
Reproduce the maintainer-provided ContainerLogs mockup faithfully: dark card
(#0c0c0d), his header + single toolbar row styling, his custom search box /
'Filter search results' checkbox / Copy+Download buttons, his toggle-button
style (Line numbers / Timestamp / Wrap — no Auto refresh), and the dark log
area with a right-aligned line-number gutter. Palette carried inline in this one
component (deliberate dark log-viewer design).

Deviations from the mockup, by design: fonts/sizes use the project scale and
monospace (no Google Fonts / hardcoded Inter/JetBrains); real streaming data via
useLogViewer rendered as safe React span nodes (no dangerouslySetInnerHTML);
mock page chrome dropped (Portainer's page provides breadcrumb/title); the
datetime range keeps the functional react-datetimerange-picker. Live-tails by
default; selecting an upper bound in the range picker shows a bounded snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:41:23 +03:00
agent_coder 9a6dd0c408 feat(logs): match EE reference — combined datetime range picker, toggle icons
Close the visual gaps vs the maintainer's reference viewer:
- Replace the two separate From/To DateTimeField controls with a single combined
  datetime range picker (@wojtekmaj/react-datetimerange-picker, sibling of the
  react-datetime-picker / react-daterange-picker already used), from-to with time
  in one control — mirrors the existing DateRangePicker wrapper.
- Add icons to the Line numbers (List), Timestamp (Clock) and Wrap lines
  (WrapText) toggles (Auto refresh already had one).
- Line numbers gutter on by default.

Adds one dependency (@wojtekmaj/react-datetimerange-picker); all its transitive
deps were already present via the sibling pickers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:50:41 +03:00
agent_coder eb35e9c47f feat(automation): configurable webhook notifier for automation events
Add an opt-in webhook notification for container-automation events (image
update, rollback, update-failed, auto-heal restart), plugging into the existing
Notifier seam in notify.go.

- Settings: new ContainerAutomation.Notification.WebhookURL (shared across
  update + heal), persisted and validated in the settings update handler
  (optional; http/https only; accepts the {{message}} placeholder).
- webhookNotifier reads the current URL from the datastore per event (UI changes
  take effect without a restart). If the URL contains {{message}} it substitutes
  the URL-encoded message and issues a GET; otherwise it POSTs the message as the
  body. Delivery, the env/stack name lookups, and any panic run in a goroutine
  under recover() with a 10s timeout — strictly best-effort, never blocks or
  crashes the automation daemon. multiNotifier fans events to logNotifier +
  webhook and isolates a panic in any one notifier.
- Message format (maintainer's spec):
    Environment | <env>
    Stack [<name>]            (Container [<name>] for non-stack events)
    Update [<name>]: <old> -> <new>
  Auto-heal: 'Auto-heal: restarted unhealthy container'.
- New NotificationPanel in settings to configure the URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:31:18 +03:00
agent_coder 7f02d20e54 feat(logs): React log viewer for container logs
Replace the legacy AngularJS <log-viewer> on the container logs page with a
modern React log viewer, reusing the existing streaming (#6) and formatting/
coloring pipeline. Features: line-number gutter, zerolog level + key=value
coloring (from the existing formatter spans), from/to datetime range, Lines
limit, Line numbers / Timestamp / Wrap lines toggles, Auto refresh (live tail
on/off), Search + 'Filter search results', Copy, Download logs, and fullscreen.

The viewer is source-agnostic (StreamLogsFn), so service/task logs can adopt it
later; this PR wires container logs only. containerLogsController.js no longer
opens its own live stream (React owns fetching now), preventing a double stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:05:56 +03:00
agent_coder 6aecdfbe46 feat(containers): interactive image-status badge (click to update / re-check)
Make the container image-status badge actionable, matching native Portainer:
- Clicking "Update available" opens the update confirm dialog and runs the
  existing update flow (standalone recreate-with-pull / stack redeploy), gated
  and disabled while in flight to avoid a double submit. The confirm+apply logic
  is extracted from UpdateNowButton into a shared useApplyContainerImageUpdate
  hook so the details button and the list badge share one implementation.
- Clicking "Up to date" re-queries the registry. Because the server caches image
  status (statusCache 5m + remoteDigestCache 5s), a plain refetch was a no-op, so
  the endpoint gains an optional ?force=true that bypasses BOTH caches for a
  manual re-check while still repopulating them; the default (auto badges + the
  auto-update daemon) keeps using the caches unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:04:49 +03:00
agent_coder f379e8057e fix(stacks): keep stack breadcrumb on container Quick Actions
The stack container list reuses the shared containers datatable, whose
Quick Actions column linked to the global docker.containers.container.*
states with only {id,nodeName}. Clicking Logs/Stats/Console/Inspect/Attach
from within a stack therefore jumped to the global route and collapsed the
breadcrumb to "Containers > <name> > Logs", losing the stack trail that
PR #7 added.

Thread the current stack route params (via RowContext) down to
ContainerQuickActions so, when rendered inside a stack, its links target the
stack-scoped docker.stacks.stack.container.* sub-tab states (reusing #7's
buildStackContainerLinkParams / STACK_CONTAINER_STATE_NAME helpers). The
global containers list and service tasks pass no stack params and keep the
global links unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:52:44 +03:00
claude code agent 474c41ec8e Merge remote-tracking branch 'origin/develop' into feat/2-stream-logs
# Conflicts:
#	app/docker/views/containers/logs/containerLogsController.js
2026-07-01 03:53:45 +03:00
vvzvlad 0e572f4ccc Merge pull request 'fix(stacks): keep stack breadcrumb trail when opening a container from a stack (#4)' (#7) from feat/4-stack-breadcrumbs into develop
Reviewed-on: #7
2026-07-01 02:23:40 +03:00
claude code agent 9205099f14 fix(logs): restore a stock-style log-viewer layout (drop the header cram)
The previous round crammed every control into the widget header as one flex
row, which read as cluttered. Restore the stock Portainer two-row shape:
- header bar: the "Logs" title on the left; Search + Copy + Download logs
  right-aligned in the header's transclude slot;
- a single clean horizontal toolbar in the widget body: Since / Lines /
  Wrap lines / Display timestamps (no longer stacked form-groups);
- the log <pre> pane below, unchanged.

Auto-refresh and the line-selection controls stay removed (already gone from
the controller). Template-only change; no controller edits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:41:01 +03:00
claude code agent 0bf4e71b79 fix(stacks): keep the stack breadcrumb trail on container attribute sub-tabs
When a container is opened from a stack, the detail tab kept the stack
trail (PR #7) but the attribute sub-tabs (Logs, Stats, Inspect, Console,
Attach) dropped it: those tabs were registered only under the global
docker.containers.container.* tree, so navigating to one left the stack
state (and its inherited params) behind, and each sub-view set a hardcoded
"Containers > ..." breadcrumb.

- Register stack-scoped child states docker.stacks.stack.container.{attach,
  exec,inspect,logs,stats} mirroring the global ones, so the inherited stack
  params survive and the trail can be kept.
- Centralize the breadcrumb logic in containerBreadcrumbs.ts (moved out of
  ItemView, which re-exports it) and add isStackContainerState +
  getContainerSubTabBreadcrumbs + buildStackContainerLinkParams.
- ActionLinksRow links sub-tabs into the stack tree (with stack+container
  params) when opened from a stack, else the global states unchanged.
- InspectView + the logs/stats/console controllers render the stack-aware
  trail; set up-front (no name) so it survives the load window and errors.

Covers regular/external/orphaned stacks and the non-stack fallback,
matching the existing ItemView breadcrumb behavior. New unit tests in
containerBreadcrumbs.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:25:04 +03:00
claude code agent 637e96f236 fix(logs): flush docker proxy stream per chunk; trim log-viewer settings UI
Backend (the "logs arrive every ~5s / pipe clogged" bug):
- dockerLocalProxy.ServeHTTP streamed the docker socket response via
  io.Copy, which buffers ~2KB into the ResponseWriter and only flushes
  when full or on handler return. Low-throughput streaming endpoints
  (container logs follow=1, events, stats, attach) therefore arrived in
  multi-second batches. Stream manually and Flush() after each chunk so
  they are delivered live. Behaviour is otherwise identical to io.Copy
  (full-write contract, EOF handling, Debug error logging); hijacked
  attach/exec go through a separate websocket handler, unaffected.
- NewSingleHostReverseProxyWithHostHeader: set FlushInterval = -1 so the
  remote-endpoint path streams live too.

Frontend (maintainer UI asks):
- Remove the line-selection mechanic entirely (Copy-selected-lines and
  Unselect buttons, selectLine/copySelection/clearSelection, selectedLines
  state, line_selected highlight): selecting/copying is mouse-native. Copy
  (all visible) and Download stay.
- Rename the unclear "Fetch" since-selector label to "Since".
- Move the settings controls into the widget header (rd-widget-header
  default transclude slot) so they share one row with the "Log viewer
  settings" title, reclaiming vertical space for the log pane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:05:02 +03:00
claude code agent 343d36834a feat(logs): drop the auto-refresh toggle (always live) and compact settings to one row
Per maintainer request: remove the 'Auto-refresh logs' toggle entirely — logs are
now always collected (container always streams, service/task always poll). Drops
state.logCollection and its whole cascade (handleLogsCollectionChange, the
logCollectionChange binding, changeLogCollection in all three view controllers,
the log-collection-change attribute) and the now-dead manual flush-on-pause
machinery (pausedFlushCount / removeTailLines / the flush branch); pauseStream is
kept for $destroy/reconnect teardown, and the stream/poll start unconditionally.
Collapse the seven stacked settings rows into a single compact flex row
(wrap-lines, timestamps, fetch, lines, search, actions) — bindings unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 00:14:04 +03:00
claude code agent f27e44f5f2 refactor(ce): remove vestigial limitedToBE plumbing from BoxSelector (F8)
After the BE indicator was removed, limitedToBE was hardcoded false and threaded
through BoxSelectorItem.onSelect -> BoxSelector.onChange/handleSelect ->
BoxSelectorAngular.handleChange, where $setValidity(name, !limitedToBE) was a
permanent no-op (always valid). Drop the parameter from the whole chain and the
no-op $setValidity. That left formCtrl/require:'^form'/IFormController dead (they
existed only for that validity call), so remove them too — the component no longer
needs a parent form. The real on-change wiring ($evalAsync -> onChange(value)) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:26:10 +03:00
claude code agent f658d67ccb refactor(settings): extract shared parseGoDuration to drop the duplicated parser (F10)
The auto-heal floor fix copied durationPattern/unitSeconds/parseGoDurationSeconds
verbatim from AutoUpdatePanel into AutoHealPanel. Move them to a shared
SettingsView/parseGoDuration module imported by both panels — single source of
truth (mirrors the STALE_TIME dedup), so the two clients' floors can't drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:49:35 +03:00
claude code agent 5f16799b4c refactor(ce): drop orphaned OPEN_LDAP type and collapse degenerate buildUrl (F6,F7)
F6: remove SERVER_TYPES.OPEN_LDAP (read nowhere after the OpenLDAP retirement).
F7: the S3 callers that passed buildUrl(subResource, action) are gone; the only
    remaining caller uses buildUrl() with no args, so collapse it to return 'backup'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:49:32 +03:00
claude code agent 8ad42a1a45 fix(logs): seed reconnect boundary state from resume params; extract+test since parser (F10,F11)
F10: the content-exact reconnect dedup rebuilt boundaryLines only from lines
     surviving the current batch, so after one reconnect at a shared timestamp it
     forgot the dropped line — a SECOND reconnect at the same nanosecond then
     re-emitted both as duplicates. Seed lastTimestamp/boundaryLines from
     skipUntilTimestamp/skipBoundaryContents so the boundary set accumulates all
     lines ever seen at the resume ts. Regression test (fails before, passes after).
F11: extract rfc3339ToUnixNanoSince into a testable logHelper module (sinceTimestamp.ts)
     and cover it (standard ns, no fraction, sub-9 pad, >9 truncate); the controller
     imports the single shared function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:42:32 +03:00
claude code agent 9768d7bb99 fix(logs): content-exact reconnect dedup; flush partial on pause; unify since; lines fallback (F1-F5)
Maintainer pre-merge review follow-up:
F1: dedup reconnect redeliveries by EXACT boundary-line content, not just
    timestamp <= resume — a new line that merely shares the boundary nanosecond
    with a redelivered duplicate is no longer dropped (skipBoundaryContents +
    pendingBoundary). Test proves line B survives while a real dup is dropped.
F2: flush the buffered partial line on intentional pause (not reconnect) and
    strip those cosmetic lines on resume so since re-delivers the full line with
    no stale-partial twin; resume point is not advanced past the partial.
F3: unify the since param to <unix>.<nanos> for initial and reconnect.
F4: fall back to 100 lines when the Lines field is cleared (avoid tail=all).
F5: memoize the API-version pin per session; warn on frame desync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:05:18 +03:00
claude code agent e9fae32b43 test(stacks): cover orphaned branch; name the stack-container state; type link params (F1,F2,F4)
Maintainer pre-merge review follow-up:
F1: test the orphaned-stack breadcrumb branch (orphaned=true, no regular) —
    href carries stackId/orphaned, not external.
F2: extract STACK_CONTAINER_STATE_NAME so code + test share one literal.
F4: type buildStackLinkParams' return as StackLinkParams (documents the real
    shape; external stays boolean, serialized by ui-router — no runtime change).
F3 (legacy ?id= deep links) answered wontfix in the PR thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:55:41 +03:00
claude code agent 28a06e80a8 refactor(ce): finish dead-BE removal — S3 backup, user-activity, LDAP, edition markers (F1-F5)
Maintainer pre-merge review follow-up (all non-blocker dead code):
F1: delete the dead S3-backup remnants (validation, query hooks, S3-only query
    key, BackupS3Model/Settings types) — kept the CE file-backup path.
F2: delete the orphaned user-activity services + their registration (kept the
    notifications component and routes).
F3: drop the unused buildOpenLDAPSettingsModel().
F4: drop the dead one-option ldap-options data (the selector was already collapsed).
F5: remove the dead data-edition attribute + its process.env typing; silence the
    intentional hasAuthorizations unused-params; drop the dead useRolesState meta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:52:33 +03:00
claude code agent be3bfd0513 fix(automation): maintainer pre-merge review — stale detection, daemon edge cases, parity (F1-F9)
F1: cap the image-status cache TTL at 5m (was 24h) — the cache is keyed by the
    LOCAL imageID, which doesn't change when upstream pushes a new image under the
    same tag, so the 24h TTL hid new images from both the badge and the auto-update
    daemon; a short TTL re-resolves the remote digest within the poll window.
F2: document that the update->rollback guard map is in-memory (restart implication).
F3: skip auto-update for an unnamed container when rollback is on (the endpoint+name
    keyed guard can't record it, so it would loop) — pure skipUnnamedForRollback + test.
F4: wrap the pre-update ContainerInspect in context.WithTimeout(endpointTimeout).
F5: document Reload() does not interrupt an in-flight tick.
F6: floor auto-heal CheckInterval at 1s (mirrors auto-update) + test.
F7: wontfix — migration is currently correct; namespace rework is out of scope.
F8: correct the misleading SSRF/AllowList comment (no filter is applied).
F9: front auto-heal interval floor + test; dedup STALE_TIME; fix invalidation comment.
Also refresh three stale '24h/long-lived cache' comments to match the 5m TTL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:51:15 +03:00
claude code agent da6933c218 refactor(logs): collapse no-op ternary, drop speculative export, fix stale comment (F8,F9,F10)
F8: formatJSONLogs plain-text fallback — both arms of `withTimestamps ? rawText
    : text` yield rawText (text === rawText when !withTimestamps), so use rawText.
F9: controllerLogsController comment referenced the old 'Live logs' label removed
    by F7 — update it to 'Auto-refresh logs'.
F10: stripHeadersFunc has no external importers — drop the speculative export.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:37:37 +03:00
claude code agent 90f51d48bb refactor(ce): drop orphaned access role column + no-op lodash compact (F6,F7)
F6: delete AccessDatatable/columns/role.tsx — the BE-only role column lost its
    only importer when useColumns stopped importing it; zero importers remain.
F7: useColumns wrapped only always-truthy helper.accessor results in _.compact,
    a no-op; return the plain array and drop the now-dead lodash import (same
    collapse already done in the parallel Wizard column files).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 15:08:01 +03:00
claude code agent d520aec159 docs(logs): neutral auto-refresh toggle label/tooltip in shared log viewer (F7)
logViewer.html is shared by the container, service and task log views, but only
the container view is a live HTTP stream — service/task still poll. Revert the
toggle wording to a mode-neutral 'Auto-refresh logs' / 'pauses log collection'
so it is accurate for both, keeping the added auto-scroll clarification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:42:19 +03:00
claude code agent 922f506fe5 feat(automation): guard update→rollback loop; name Settings types; tests & doc fixes (F1-F7)
F1: record rolled-back targets per service (endpointID/containerName + remote
    digest) and skip auto-update during a 24h cooldown unless the remote digest
    changes — breaks the infinite update→rollback loop on a persistently
    unhealthy image, without blocking a genuinely new image.
F2: unit-test applyContainerUpdate dispatch/payload mapping.
F3: settings_update.go comments mention auto-heal AND auto-update.
F4: drop stale '(future M4)' TS docs; primitives are frontend-only.
F5: replace the anonymous ContainerAutomation settings struct with named
    types (identical JSON tags).
F6: drop parseEnable (duplicate of boolLabel).
F7: remove the unused gitService dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:29:57 +03:00
claude code agent b1b09e5da0 refactor(ce): remove leftover dead BE code — gates, orphans, dead selectors/CSS (F1-F4)
F1: drop the two HomeView edition-gate panels + their files (License/BackupFailed).
F2: delete zero-importer orphans (edition mutation, HubspotForm, HomepageFilter,
    relations mutation, ActivityLogsView cluster, ExperimentalFeatures subtree).
F3: collapse single-option selectors (Backup settings, init restore, env types)
    and delete the option files they orphaned.
F4: remove dead BE-teaser CSS rules and the --BE-only variable.
Also drop the orphaned .btn-warninglight BE-teaser variant.
F5 (limitedToBE) intentionally left — it is still read by BoxSelectorAngular.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:13:55 +03:00
claude code agent f4f296fc05 fix(logs): drop partial line on reconnect, stabilize poll row ids, cover CRLF/dedup (F1-F6)
F1: stop emitting/committing an unfinished line in onEnd/onError reconnect
    paths; since-based reconnect redelivers the full line.
F2: give service/task poll rows positionally-stable ids so track by log.id
    reuses DOM rows and text selection survives the 3s poll.
F3/F4: tests for CRLF stripping and reconnect-dedup across separate chunks.
F5: correct the stale refreshRate comment.
F6: unroll the side-effecting IIFE-in-ternary into if/else.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:08:46 +03:00
claude code agent a1851417d1 test(stacks): cover external-stack breadcrumb branch in buildStackLinkParams (F1)
Add a test case driving the external-stack branch (external='true', no DB
stackId) and assert the back-link carries external=true/type and omits
stackId/regular. stackId/regular are set in the route params so the negative
assertions actually catch a fall-through-to-regular regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 14:06:25 +03:00
claude code agent 70f7fe5e84 Merge remote-tracking branch 'origin/feat/10-update-now' into feat/3-auto-update 2026-06-29 12:48:24 +03:00
claude code agent cdf17d904d fix(automation): rollback robustness — transient inspect, start_period, digest images, shutdown, event order (#12 review)
F1: tolerate up to 3 consecutive health-gate inspect failures (reset on
success) before declaring an update failed, so a transient Docker API blip no
longer triggers a false rollback.

F2: detect baseCtx cancellation during the gate and abort without rolling back
or emitting update-failed (debug log only), instead of a misleading
"rollback failed" event on every shutdown mid-gate.

F3: derive the gate deadline as start + max(RollbackTimeout, StartPeriod+buffer)
via effectiveRollbackDeadline, reading the container's healthcheck StartPeriod
so a legitimately slow-starting container is not rolled back while starting.

F4: only enable the gate when the original reference is a proper tag (new
isTagReference helper); skip with a log line for digest-pinned / bare-image-id
containers that cannot be re-tagged.

F5: document the sequential-tick delay limitation of the gate poll.

F6: emit EventUpdated only after the gate confirms healthy (or immediately when
no gate is active); the rollback path emits only EventRollback, so the event
sequence is truthful.

F7: floor RollbackTimeout at 10s in backend and frontend validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:57:54 +03:00
claude code agent 32a2b7a9ae feat(automation): health-gated rollback + per-endpoint + notify hook (#12, epic #3 M5)
P0 Health-gated rollback (standalone auto-update path): capture the previous
image id + reference + healthcheck before the recreate, then poll the new
container's health over a configurable window. On healthy proceed (and only
then clean up the old image); on unhealthy/exit/timeout re-tag the old image
back onto the original reference and Recreate (no pull) to restore it, reusing
Recreate's config preservation. The decision is a pure decideRollback() helper.

P1 Per-endpoint enable: ContainerAutomationDisabled flag on Endpoint (zero value
participates, no migration churn), checked by both daemons; settable via the
endpoint update API. UI control deferred (see report).

P2 Notifier seam: minimal Notifier interface + logNotifier, emitting structured
updated/rollback/update-failed/heal-restarted events from the daemon.

Settings: RollbackOnFailure + RollbackTimeout (default 120s) added to
ContainerAutomation.AutoUpdate, wired through defaults/migration/golden,
settings_update validation, the AutoUpdatePanel and the TS types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:41:55 +03:00
claude code agent 21b5ec3e05 fix(automation): git-stack honesty + ECR registry refresh + interval floor (#11 review)
F1: Stop routing git-backed stacks through a per-tick RedeployWhenChanged for
image-only updates. The git redeploy path short-circuits when the commit is
unchanged (so an upstream-digest update never applies) yet still git-fetches
every tick. Git stacks are now detect-only in the auto-apply path; their image
update lands on the next git change or via manual "Update now". File (non-git)
stacks still force-pull-redeploy immediately. The AutoUpdatePanel text no longer
promises daemon auto-update for git/externally-managed containers.

F2: Resolve registries for the file-stack redeploy the same way the established
userless/system path (RedeployWhenChanged) does, via the new
deployments.ResolveStackRegistries: scope to the stack author's endpoint access
and RefreshAndPersistECRTokens, instead of hand-passing Registry().ReadAll().
ECR-backed stacks now auto-update with fresh tokens.

F3: Add a 1m floor for the auto-update poll interval, enforced in the settings
Validate and mirrored in the frontend validation.

F4: Thread the application shutdownCtx into NewService and use it as the base
for the heal/update job operation contexts, so shutdown cancels in-flight work.

F5: Correct the updateEndpoint comment about monitor-only badge-cache warming
(only in-scope monitor-only containers are status-checked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:24:58 +03:00
claude code agent b3ae5f3659 feat(automation): native auto-update daemon (#11, epic #3 M4)
Add an optional periodic auto-update daemon that detects outdated container
images and applies updates, replacing the containrrr/watchtower sidecar. It
extends M1's containerautomation service/scheduler/labels infrastructure and
reuses the existing zlib image-detection engine, the standalone Recreate path
and the stack deployer.

Backend:
- api/containerautomation/autoupdate.go: scheduler job iterating Docker
  (non-edge) endpoints -> in-scope running containers -> ContainerImageStatus;
  for Outdated: standalone -> ContainerService.Recreate(pull); stack-managed ->
  one stack redeploy-with-pull per stack per tick (git via RedeployWhenChanged,
  file via the deployer directly); external compose -> detect only. Monitor-only
  containers are status-checked (warms the badge cache) but never applied.
  Overlap guard (atomic), pull/registry-auth failure -> leave running container
  untouched, conservative cleanup of the dangling old image on the Cleanup flag
  (non-forced ImageRemove only succeeds when truly unused).
- labels.go: update enable / monitor-only labels with watchtower aliases,
  InUpdateScope, IsMonitorOnly, and pure resolveContainerUpdateRouting /
  groupContainersForUpdate (Go analogue of M3's TS routing + grouping).
- service.go: run both jobs, Reload restarts/stops each per settings; NewService
  also takes ContainerService, StackDeployer and GitService.
- Settings.ContainerAutomation.AutoUpdate {Enabled, PollInterval, Scope,
  Cleanup} with fresh-install defaults and a 2.43.0 backfill (extends M1's
  migration; golden test data updated). settings handler validates + reloads.

Frontend:
- Global AutoUpdatePanel in SettingsView (enable / poll interval / scope /
  cleanup) via useUpdateSettingsMutation, plus settings TS types.
- Read-only per-container Auto-update row in the container details view
  (Docker labels are immutable at runtime), surfacing monitor-only.

Tests: Go unit tests for the update label aliases, scope, monitor-only, the
routing decision and the one-redeploy-per-stack grouping; vitest for the panel
and the per-container row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 10:04:09 +03:00
claude code agent ccd5897915 fix(automation): gate stack redeploy on PortainerStackUpdate + bulk/name polish (#10 review)
F1: single-container "Update now" and bulk "Update" now require
PortainerStackUpdate when the resolved path is a stack, disabling the
action with a tooltip / skipping it rather than letting the click 403.

F2: resolveContainerUpdatePath only matches a Docker Compose stack; a
same-named swarm/kubernetes stack is treated as external.

F3: SecondaryActions no longer renders an empty ButtonGroup when all of
recreate/duplicate/update-now are hidden.

F4: bulk update reports an explicit no-op toast and counts containers vs
stacks honestly in the success summary.

F5: bulk toasts use trimmed container names (no leading slash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:42:33 +03:00
claude code agent f7cb0f3241 feat(automation): "Update now" action (stack-aware) + bulk update (#10, epic #3 M3)
Add a discoverable per-container "Update now" action, shown only when the
image status is `outdated`, plus a bulk "Update selected" action in the
containers list.

Both manual paths share ONE apply primitive (applyContainerUpdate /
useUpdateContainerImage) that also backs the future M4 auto-update job:

- standalone container  -> recreate-with-pull (existing recreate endpoint)
- stack-managed         -> stack redeploy-with-pull (existing git/file stack
                           update mutations), so the container stays in its
                           stack and is never recreated out-of-band
- externally-managed    -> refused; the details button is disabled with an
  compose                  explanatory tooltip and the bulk action skips it

Decision logic lives in the pure, unit-tested resolveContainerUpdatePath /
groupContainersForUpdate helpers. The bulk action filters to outdated
containers and redeploys each owning stack exactly once even when several of
its containers are selected, reporting per-item success/failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 09:24:10 +03:00