- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Opening a container from a stack's Containers table showed
"Home > Containers > <container>" instead of keeping the stack trail,
so the user could not navigate back to the stack.
Two root causes are addressed:
1. Route param collision: docker.stacks.stack used the query param `id`
for the numeric stack DB id, while its child docker.stacks.stack.container
uses the path param `id` for the container id. Navigating into a container
overwrote the stack id. The stack id param is renamed `id` -> `stackId`
everywhere it is read or written (route url, stacks datatable link,
create-stack redirect, gitops workflow card link, stack ItemView reader).
2. Hardcoded breadcrumbs: the container details ItemView always rendered the
global "Containers" crumb. Breadcrumbs are now state-aware: when reached
via docker.stacks.stack.container the stack trail
(Stacks > <stack> > <container>) is rebuilt from the inherited stack params,
honoring external/orphaned stacks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Container log live-stream review fixes (frontend only):
- F1/F2: demux Docker's multiplexed (non-TTY) stream at the BYTE level by
frame length, decoding only payloads. Previously the stream was text-decoded
whole and cut on '\n' before stripping 8-byte headers, which desynced when a
length low-byte was 0x0a or a header byte was >= 0x80. streamContainerLogs
now hands the processor raw Uint8Array chunks; createLogStreamProcessor is
rewritten to parse frames, concatenate payloads, split lines on 0x0a, and
UTF-8-decode complete lines. formatLogs is called without stripHeaders so
headers are not stripped twice. Added explicit byte-frame tests.
- F3: request timestamps=1 internally and resume reconnects from the parsed
RFC3339 timestamp of the last line (not client wall-clock); strip the prefix
before display when the user's timestamps toggle is off; dedup the inclusive
`since` boundary lines on reconnect.
- F4: run the fetch stream URL through dockerMaxAPIVersionInterceptor so it
matches the axios getContainerLogs version pinning.
- F5: notify on stream error once per reconnect loop, not every 3s retry.
- F6: resuming Live no longer wipes the buffer (startStream(false)) and
continues from `since`.
- F7: service/task logs still poll; documented the re-render limitation
(out of scope: issue #2 is container logs).
- F8: flush the trailing partial line on the error path too (parity with onEnd).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the 3s $interval polling of container logs with a live HTTP
stream, and stop re-writing already-rendered lines (fixes selection bug).
- streamContainerLogs (containers.service.ts): fetch + ReadableStream
reader with follow=1, same-origin credentials:'include' (httpOnly JWT
cookie; CSRF only guards mutations), agent-target / manager-operation
headers replicated for Agent/Edge, AbortSignal-driven lifetime.
- containerLogsController: stream instead of poll; append parsed lines
into the buffer (push, never replace), cap at 5000 lines trimming from
the head; AbortController on pause/destroy/param-change; reconnect with
3s backoff resuming from `since` (dropping tail) on stream end/error;
Live toggle pauses/resumes the stream; tail/since/timestamps changes
restart the stream.
- log-viewer: `track by log.id` (was $index), filtering moved out of the
template into the controller (applyFilter via $watchCollection), removed
inert force-glue, decoupled auto-scroll from log collection, relabelled
"Auto-refresh logs" -> "Live logs", clearer empty states.
Backend unchanged (logs already stream transparently through the Docker
proxy). Shared task/service log views keep working via the new id'd lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for append-only log rendering and HTTP log streaming.
- FormattedLine gains a stable, monotonically increasing `id` (new
lineId.ts sequence), assigned centrally in formatLogs. Internal
formatters now return id-less FormattedLineContent. This lets the
viewer use `track by log.id` so already-rendered rows are never
re-bound (fixes the text-selection collapse).
- formatJSONLine: runtime guard so a bare JSON string/array log line
falls back to plain text instead of rendering Object.keys as
`0=h 1=e ...`.
- createLogStreamProcessor: stateful demuxer that buffers streamed text,
emits only complete lines (carrying the partial remainder), and reuses
formatLogs/stripHeadersFunc for Docker 8-byte frame demux.
- Unit tests for the demuxer, stable-id assignment and the JSON guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unwrap be-feature-indicator / limited-feature directives and feature-id
attributes from LDAP/OAuth/access-management templates, delete BE-only
AngularJS views (Active Directory, OpenLDAP, RBAC access-viewer, auth
logs) and remove their registrations/routes and the r2a teaser-prop
allow-lists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Upgrade-to-Business banner, BE sidebar items (Licenses, Shared
Credentials, Edge Configurations, Waiting Room, Update & Rollback), BE
branding (BE logo/footer), and BE-only routed views (update-schedules,
EdgeAutoCreateScript, WaitingRoom, TimeWindowDisplay/Picker). Prune the
featureId/feature/BEFeatureID teaser props from shared components
(Switch, SwitchField, BoxSelector, TooltipWithChildren, wizard Option)
and fold isBE in useUser while preserving CE authorization semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>