Repository navigation
v1.3.0
Added
-
The reporter helper forwards only reporter routes, and no regex can be made to backtrack on a long input (#668, part of #577). The helper that carries the mod's calls over mutual TLS now answers
403to any path or method that is not one of the reporter calls, and builds the request it sends from the matched route. Twelve regexes that could take quadratic time on a long run of slashes or blanks (a trailing-slash trim on a request path took 3.4 s on 100,000 slashes) are replaced with string handling, and the four confirmation buttons on Admin > Certificates and People take focus in code instead of withautoFocus. -
Failed requests are limited per address (#667, part of #577). Ahead of every
/apiroute, one address may make 300 requests a minute that end in a response of 400 or more. The next one gets429withRetry-Afteruntil the minute is over, and the first refusal in a window is logged asauth.throttled. Successful requests are never counted, andPODIUM_FAILURE_LIMITchanges the limit. It is a flood brake on the paths that check a token or a certificate, which CodeQL reported as unlimited (js/missing-rate-limiting); the wrong-secret lockouts are unchanged. New dependency:express-rate-limit. -
A built-in TLS guide: browser trust, the reporter, expiry and the threat model (#590, part of #577).
docs/SERVER.mdhas a new "Built-in TLS" section that walkspodiumctl start --tls, the two ports, that no browser needs the CA, the reporter config, the HTTP switch, what to do when a person's certificate, the serving certificate or the CA expires, and what TLS stops and does not stop.docs/REPORTER.mdhas a table of what each reporter status-line message means, and the User guide's "Something is wrong" table points to both. Uploading your own certificate is not supported. The guide was walked on a throwaway server with Claude Code sessions in tmux, which found three reporter defects, filed as #645, #646 and #647.docs/DECISIONS.md42 no longer says nothing is built, anddocs/SERVER.mdsays a wildcard bind adds no address to the serving certificate. -
The Certificates page says why Rotate CA is off for a CA made before rotation with an overlap (#638, part of #577). OpenSSL refuses the chain
[leaf, cross]under a CA withpathlen:0, which is what the first TLS builds made. For such a CA the confirmation now explains that it cannot be rotated with an overlap, disables Rotate CA with that reason beside it, and leaves Rotate CA and end the overlap now, which replaces it with a CA that can take a planned rotation next time.GET /api/admin/certificateshas a newcanOverlapfield. -
Admins can stop reporters from connecting over HTTP, with a warning while they can (#589, part of #577). Admin > Server > Certificates has a new section, Reporters over HTTP, with the switch Allow reporters to connect over HTTP (
tls.allowReportersOverHttp). It is on for a server that already had reporters when TLS was first turned on and off for a fresh one, and a warning line shows in both states. Turning it on is immediate. Turning it off asks first, names how many reporters are connected over HTTP now, and says the console on loopback keeps working. With it off, the HTTP listener answers403to the routes a reporter calls (/api/fleet/report,/hook, and the peers and respond routes) from any address but loopback, logstls.http_refused, and shows the source in the Reporters page's refused list; a reporter that has a config uses HTTPS and is unaffected, and the console is never gated. A change is written to the Admin log astls_http_changed.podiumctland the Containerfile already publish both ports (#580). -
Rotate the CA with a cross-signed overlap, and retire the old one (#588, part of #577). On Admin > Server > Certificates, Rotate CA… asks first and then makes a new CA, signs a cross certificate for it with the old one (120 days), and serves
[new leaf, cross certificate], so a reporter that holds only the old CA still verifies the server. Both CAs are trusted for client certificates. A reporter that renews during the overlap gets the new CA and the cross certificate in the renewal response (rotation) and adds the CA to its config file only if the cross certificate verifies under a CA it already holds and the new certificate under the new one; it never replaces a CA. The page shows a blue "rotation in progress" band and a Previous certificate authority row with "N of M reporters still use it", and Retire old CA… names that count, refuses those reporters at once and closes the connections that came in on the old CA. Rotate CA and end the overlap now is the hard cutover after a leaked key: no overlap, nothing delivered, every reporter needs a reissued config, and every open connection with a client certificate is closed. The listener, the serving renewer and the certificate check now read one list of trusted CAs, which closes the T5 review's concern that a certificate could pass TLS and be refused for its issuer. A new CA haspathlen:1; a CA made earlier (pathlen:0) can only rotate by ending the overlap. New routesPOST /api/admin/certificates/rotate-caand/retire-ca;GET /api/admin/certificatesaddsrotationandoverlapDays. Mockup:mockup/podium-tls-certificates.html(#573), state "CA rotating". -
Admin > People shows each credential, one Actions menu per row, and the reporter config after Create user (#587, part of #577). The table has a Reporter certificate cell and a Console token cell, each with its own badge and a note that opens on hover and on focus (the date and what happens next for the certificate,
pdm_<key id>for the token); the Status column is gone, the Group select sits under the name and "created" under Last report. Every row has one Actions menu, grouped Console token, Reporter certificate and Person, with Up, Down, Home, End and Escape; a rotate, a revoke, Revoke certificate, Revoke all and Reissue config ask first in a full-width row under the person. After Create user, Reissue config or a token rotation the show-once box gives the token and the filereporter-config-<name>-<epoch>.pemwith Download reporter config, once; Download certificate gives the public certificate and the CA, never a key.GET /api/admin/peopleaddstls,graceDaysand each row'scertificate(notAfter,revoked). With TLS off the certificate cell says so and the certificate actions are disabled. Revoke token still revokes the person's certificate too (SPEC 7.7), so its confirmation says the reporter is refused; the mockup's "Their reporter keeps working" for it is not followed.
Standards: the row Actions menu (.menu,.menubox,.mi,.mgh,.msep), the confirmation in a full-width row under a table row (tr.confirmrow), the show-once box's second part (.once h3andol.plain), and the People table's two credential cells with the Group select under the name; approved with the mockup on #573 (decisions 11, 15 to 19, 22 and 27). -
Admin > Server > Certificates and the certificate pill in the header (#586, part of #577). The new page shows the CA and the serving certificate with their dates and a status that is a shape, a word and a color, a band at the top (blue, orange or red as the first of them runs out), the reason a renewal failed in a critical row, the CA fingerprint with a copy control and Download CA, and the serving certificate's names in a small dialog. The serving certificate's lifetime and renew-before days, a person's certificate lifetime and grace days save when the field is left, with a refused value shown in place, and the header countdown has its own switch. Renew now works from the page, the certificates Podium kept are listed, and an expired CA lists the steps to restore access. The header shows a pill after the connection pill, blue at 30 days or fewer, orange at 10, red at 5, and "Certificate expired", for whichever of the CA and the serving certificate expires first. It links to the page and has no X; the switch hides only the countdown, and a failed renewal or an expired certificate always shows. It reads the new public
GET /api/tls/status, andGET /api/admin/certificatesnow also returns the CA, the header setting and a person's certificate lifetime and grace (reporter). Rotate CA... is on the page but disabled until the rotation lands (T10); People, the HTTP switch and the rotation state follow in T9 to T11. Mockup:mockup/podium-tls-certificates.html(#573).
Standards: header status pills.pill.soon(blue) and.pill.serious(orange), and a status pill that is a link (a.pill); Admin table badges.badge.soon,.badge.seriousand.badge.badwith an icon inside a badge; the red band row.band.critand the band row's text layout on an Admin page (.band .bt,.band .bd);.nsetand.notefor a grouped list and a quiet note in a small modal; a focus ring on the tooltip trigger (.tip:focus-visible); andtd.nwand the CA fingerprint line's wrap. Orange text uses the existing band color (--seriousmixed with--ink), since no--serious-texttoken exists. Approved with the mockup on #573 and in the decisions comment on #323 (#323 (comment)). -
A person's certificate can be renewed once an hour (#612, part of #577).
POST /api/reporter/renewanswers429withRetry-Afterand the codeTOO_SOONwhile the person's newest live certificate was issued less than an hour ago, before it startsopensslor adds a row, so a looping or hostile client holding a valid certificate cannot fillperson_certificatesor keep the server busy. The limit is fixed at an hour, not a setting. The helper treats a429as a wait, not a refusal: the request still goes out, the file is left alone, and it asks again after theRetry-Aftertime (never sooner than a minute). A refused renewal is logged astls.reporter_renew_refusedwith the code. -
The reporter renews its own certificate, and the server checks the grace (#585, part of #577). A person's certificate lasts
tls.reporter.lifetimeDays(90; 30 to 365) and the helper renews it when 30 days of it are left, at start, before a request, and every 10 minutes while a session idles. It makes a new EC P-256 key and sends only the public key to the newPOST /api/reporter/renew, which is answered to a client certificate and nothing else. The server signs that key for the same person and returns the certificate and the CA(s). The helper checks the answer (our key, signed by a CA the file already holds), then rewrites the config file in one rename at mode 0600 under a lock file (<file>.lock, taken over after a minute if its owner died), so several sessions renewing at once leave one valid file and the second takes the first's certificate. The old certificate stays live until it expires, and each is its own row. An expired certificate may still renew fortls.reporter.graceDays(30; 0 to 90) after it expired, and the status line says it is renewing. The server never trustsCERT_HAS_EXPIREDor a serial for that: the certificate's bytes must equal the stored one and its signature must verify under the CA key that issued it (X509Certificate.verify), so an expired forgery with the same subject and serial, from a fake CA or self-signed, is refused. Past the grace, or for a revoked certificate, the status line and/podiumsay to ask an Admin to reissue the config. Both settings can be saved withPUT /api/admin/certificates(the page that shows them is T8, #586). A CA arriving in the renewal response is T10 (#588); this step never adds a CA the file does not already hold.PODIUM_RENEW_BEFORE_SECONDSandPODIUM_RENEW_CHECK_SECONDSchange the helper's thresholds for tests. -
The reporter connects over TLS with the person's certificate (#584, part of #577). When a reporter config file exists (
PODIUM_REPORTER_CONFIG, or the newestreporter-config-*.pemin~/.podium/by its issued-at line), the mod starts a small Node helper,hooks/helper.mjs, once per session and sends its requests to it over a Unix socket with$.http.fetch(..., { socketPath }). The helper makes the HTTPS connection with the CA and the person's certificate from the file, so nothing is installed andNODE_EXTRA_CA_CERTSis not used. The server address and the name come from the file. It forces certificate verification on, clearsNODE_TLS_REJECT_UNAUTHORIZEDandNODE_OPTIONS, keeps one socket per session in a 0700 folder with mode 0600, printsreadywhen it listens, exits when its parent dies and removes only its own socket, reports a failure in anX-Podium-Helper-Errorheader, and aborts the response on an upstream error mid-body. It refuses a config file that group or others can read. It re-reads the same file only for the same person and keeps the last good copy when a read fails. A/cleardoes not stop it. The reporter findsnodebyPODIUM_NODE, then its PATH, then the usual places (Homebrew,/usr/local, nvm, Volta); a missingnode, a loose file mode, a file for another name, an expired certificate, a revoked one, or a server certificate that is expired, for another name or signed by another CA each show in the status line and in/podiuminstead of a bare "offline". It never sends a credential overhttp://when the address washttps://. With no config file nothing changes. -
Each person gets a certificate, and an Admin can revoke it (#583, part of #577). With TLS on, creating a person also returns their reporter config file once,
reporter-config-<name>-<epoch>.pem: the CA, a 90-dayclientAuthcertificate (subjectCN=pdm_<key id>, O=podium, never the name) and its private key, with the server address in the header. Podium keeps only the certificate, by SHA-256 fingerprint in the newperson_certificatestable (migration 23), never the key. The HTTPS listener now asks for a client certificate with the Podium CA asca; a request that carries one is attributed to that person, checked on every request, so a revoke reaches a connection that is still open. A forged certificate (same subject and serial from another CA, self-signed, expired, or the serving certificate) is refused with a 401 even when a valid token is sent with it. New Admin routes (404 while TLS is off):POST /api/admin/people/:id/certificate/reissue(a new file, once; the earlier certificate is revoked),POST /api/admin/people/:id/certificate/revoke,POST /api/admin/people/:id/revoke-allandGET /api/admin/people/:id/certificate.pem(the certificate and the CA, never a key). Revoking only the certificate leaves the token and the console sign-in alone; Revoke token also revokes the certificate, so it and the new Revoke all stop both. Both now refuse to revoke the last active Administrator. Fixes a bug waiting in #582: a renewed serving certificate dropped the CA list, so client certificates would have stopped verifying after the first renewal. The reporter that uses the file is T6 (#584) and the screens are T9 (#587). -
The serving certificate renews itself, with two Admin settings (#582, part of #577). With TLS on, the server checks the serving certificate at start and once a day and renews it when fewer than
tls.serving.renewBeforeDays(30; 7 to 90) remain, from the same CA and for the same names, with a lifetime oftls.serving.lifetimeDays(365; 30 to 825). The new certificate is swapped into the running listener withsetSecureContext, so nothing restarts: an open connection finishes on the old certificate and a new one gets the new. The replaced pair goes totls/previous/and the last 3 are kept. A failed renewal is logged astls.renew_failed, kept for the Admin API and retried at the next daily check. New Admin routes (404 while TLS is off):GET /api/admin/certificates,PUT /api/admin/certificates(a value outside its range is a 400 that names the range; each change is written to the Admin log) andPOST /api/admin/certificates/renew, which also takes up a changed bind address orPODIUM_ALLOWED_HOSTS. The Certificates page that shows this is T8 (#586). -
The server makes its own CA and serving certificate and serves HTTPS (#581, part of #577). With
PODIUM_TLS=on(podiumctl start --tls), the first start creates a CA (10 years) and a serving certificate (1 year) intlsbeside the database (/data/tlsin the container) withopenssl, EC P-256, the folder mode 0700 and keys 0600, then opens HTTPS on port 4318 (PODIUM_TLS_PORT) next to HTTP on 4317. The certificate names arelocalhost,127.0.0.1, the bind address andPODIUM_ALLOWED_HOSTS, and the Host check applies on HTTPS too. A CA that exists is never overwritten: a half-present or unreadable one, a missingopensslor a bad or busy port logstls.failedand leaves HTTP working. A restart reuses the same CA and certificate. The log linestls.ca_created,tls.serving_createdandtls.listeningcarry the CA's SHA-256 fingerprint and dates. Nothing is shown in the console yet, and renewal and person certificates come in later steps (#582, #583). -
podiumctland the image are ready for built-in TLS (#580, part of #577).podiumctl start --tls(orpodiumctl config set PODIUM_TLS=on) turns TLS on; it is off by default and nothing listens on HTTPS yet, because the server's CA and HTTPS listener come in the next step (#581). With it on, container mode publishes the HTTPS port (4318, orPODIUM_TLS_PORT) next to 4317 and mounts~/.podium/tlsat/data/tls, so the CA will survive a restart, a rebuild and a removed volume and can be backed up.podiumctlwarns when that mount is missing, and the server logstls.storage_not_persistentwhen/data/tlsis not on a named volume or bind mount.podiumctl rmnow says the TLS folder is kept andrm --purge-tls --yesdeletes it;statusshows atls:line. The image carriesopenssland exposes 4317 and 4318. Standalone mode makesdata/tls(mode 0700) beside the database. -
Fold the Plan / Files column on the orchestrator page (#604). A button on the column's edge hides the plan and files so the Agents section takes the full width, and a slim rail with the same button brings them back. The choice is kept in the browser for every orchestrator's page, and the selected tab and scroll position are there when the column opens again. Stacked at a narrow width, the button hides the panel in the stack.
Standards: new.fold-col,.fold-clip,.fold-taband.fold-rail(the Plan / Files column's wrapper, the box that clips the hidden panel, its fold button built on.tbar-tabturned 90 degrees, and the rail left while folded), and.two.folded; the plan-panel sample's selector moves to.two > .fold-col > .fold-clip > .sec. Approved by the maintainer when the mockup was accepted on #604. -
Previous and Next in the file diff modal (#630). The diff window now has the Files tab's pager at its lower left: "Showing file 3 of 12" and Previous and Next buttons that step through every file in the list, in its order, without closing the window. Each button is named for the file it opens, and the first and last file disable the matching button. The title, counts and diff change in place, the scroll returns to the top and the Wrap lines choice stays. Only the file in view is asked for, a moment after the last step so that holding a key down does not queue a request for every file it passes, and a diff that arrives for a file you have already left is dropped. At a narrow width the switch moves up beside the pager.
-
A Wrap lines switch in the file diff modal (#623). Long lines in a file's diff now wrap at the pane edge by default, and a Wrap lines switch at the lower right of the modal turns that off for today's sideways scroll. A wrapped line continues under its own text, with its number and the + or − shown once on the first line. Side by side, the old and new halves of a changed line share one grid row, so they stay level, and keyboard and screen reader order is unchanged. The unified column (below 720 px) wraps the same way. The choice is kept in the browser and a failed read falls back to wrapped. The note moves into a bottom row with the switch, and the row shows only where a diff is drawn. Nothing is sent or stored by the server.
Standards: the.swon/off switch is used outside Admin, in the diff modal, so its rules move fromadmin.css(under.adminv) tofleet.csswith no change in Admin; its state word sits beside a label,.sw > b; and the modal gains a bottom row,.mf, holding the note and the switch. Approved with the mockup on #623 (mockup/podium-file-diff-wrap.html). -
Record a session whole and replay it (#353). The owner of a session, or an Admin, can record the orchestrator and any of its agents and forks from the orchestrator page, and play the run back later: every prompt, what the model wrote, and each tool call with its whole input and whole result. It is off by default and needs three yes answers: the Admin's new Session recording feature flag (it needs a token on the server), the choice of the session, and the reporter's new
sessionRecordingoption ("Allow session recording", off by default). Only the session's owner and an Admin can read, stop, delete or replay a recording. Everyone sees that a session is recorded, by whom and when it goes, and never what is in it, and nobody can hide that. Secrets are replaced on the machine before anything is sent, which is best effort and not a guarantee. A recording is kept 6 hours by default (1 hour to 7 days), stops at 512 MB for one session or when all recordings reach 4 GB, and goes out in 5 second batches. The limits come from a measurement with simulated traffic, not real sessions. Not captured: the model's thinking text (the mod receives it empty, tested on one model at one effort; the replay marks where it thought), and a question or permission prompt answered in the terminal leaves no decision row. This changes the report and adds a route, so the reporter must be updated too; installed copies of the mod update at the next release.docs/REPLAY.mdis the new page that explains it end to end.- Admin. Settings gains a Session recording section (keep time, disk budget, limit for one session) and a Recordings table with Stop, Open replay and Delete. Stored activity lists recordings with Source set to Recordings. Each action is written to the Admin log, never with any text.
- The picker. A Recording cell, last in the orchestrator page's facts, opens a dialog to choose the loops and start. It says when the owner has not allowed recording.
- The notice. A band on the card and a layer on the orchestrator page, with no X and no switch, and a line in the owner's terminal when recording starts, stops or hits a limit. History marks a session whose recording is kept.
- The replay. A page of its own at
#/?open=<id>&replay: loop filter, position slider, Jump to next failure, results under their calls, answers and approvals sent from Podium shown with who sent them, the newest 200 rows with Show older. Stopping keeps what was recorded. - Measurement.
scripts/podium-recording-load.mjsreproduces the write-load numbers;scripts/podium-recording-seed.mjsseeds a throwaway server with recordings.
Standards: new.band.recording(the card band) and.rec-bar/.rec-bar.kept(the orchestrator page's layer) are consent notices, not wall notices: no X, no switch, never hidden while a session records, an exception to the wall-notice rules; new.badge.recording;.fact.recwith the.rec-iring and disc (idle,.live,.kept); Machine and System join into oneName | Systemcell (.mname,.sep), superseding the separate cells approved on #427, so the grid stays twelve cells; feed rows.feed li.markand.feed li.curwith.whoand.res;.scrub, a labelled range input; the Record picker (.recmb, reusing.optsand.opt); the replay page; and the Admin Session recording controls, Recordings table and Source = Recordings in Stored activity. Approved with the mockup on #353 (mockup/podium-recording.html).
-
A nightly prerelease of
development(#528)..github/workflows/nightly.ymlruns daily at 06:00 UTC and by hand. It tests and buildsdevelopment, then publishes the tagnightly-YYYY-MM-DDand a GitHub prerelease with a source tarball of that commit, and removes the earlier nightly so one exists at a time. It publishes nothing when the tip already has a nightly or when a test or the build fails. A nightly is never the latest release, so the in-app update notice ignores it (a test now covers that). The workflow takes effect when it reachesmain, because a schedule only runs from the default branch. CONTRIBUTING.md says how to install one. -
scripts/page-shot.mjsloads a Podium page at a set width in headless Chrome (#475). One command gives a full-page PNG at 400 px (or any width and height), and--evalruns JavaScript in the page first, to open panels or report where elements sit, and prints its JSON result. It does not wait for the network to go idle, so it works against the page's open event stream, whichchrome --headless --screenshotdoes not. It starts Chrome with a throwaway profile and always stops it and deletes the profile, also when the page fails to load.--urlis required, so it is never pointed at a running server by default. CLAUDE.md and CONTRIBUTING.md say when to use it. No change to the app. -
The orchestrator's facts grid shows the machine and its system (#427). Two new cells, Machine and System, sit before Reporter, so the grid is twelve cells in two rows of six (two columns by six rows at 640 px and narrower). Blowouts moves from the start of row two to the end of row one to make room. Machine is the host name the reporter sent. System is its OS and version, such as
Fedora Linux 44ormacOS 15.4, with the same icon and wording as My reporters and Admin > Reporters, cut with an ellipsis when long. Each has a popup like Resume's and Fork's, opened by hover and by keyboard focus. A session with no machine (nouserName) reads "Not reported" in both cells and its popup says to setuserName; a machine with no OS reads "Not reported" and its popup says updating podium-reporter fills it in.
Standards: the facts grid is six columns (facts six), a cell with a popup isfact pop(Resume and Fork share it with the new cells, and the popup rules are keyed to.fact.pop > .tipso aTipinside a cell is not styled as a popup), and the system's icon and text aresysiandsys-t; approved with the mockup on #427 (mockup/podium-facts-system.html), option C with Machine before System. -
An orchestrator's page has its own address (#546). Opening an orchestrator changes the address to
#/?open=<id>, the same pattern History uses, so the browser's Back button returns to the list of orchestrators, Forward opens it again, and a reload or a shared link opens the same page. Moving between orchestrators in the rail replaces the entry, so Back still goes to the wall. An address naming a session that is not in the fleet lands on the wall with a message. -
Admin > Reporting (#484, #482, #483). A new menu group with six pages: Usage, Spend, People and groups, Sessions, Failures and Admin activity (the Data kept page moved to Admin > Server > Data, #432). Each takes a period, a day, week or month grouping and filters, states the time zone and how far back its data goes, and exports exactly what it shows as CSV or JSON (and prints). Podium now keeps daily summaries of usage, sessions, failures and the fleet for 395 days (
RETAIN_ROLLUP_MS), written before the detailed rows are swept, so a report can reach past the 30 days of sessions and events. The summaries hold counts, tokens, cost and names, and no session content. Admin > Server > Data lists what is stored and for how long.- Spend breakdown (#482). A person's row in Spend today opens what the figure is made of: by session, by model, and orchestrator against agents. Each cut adds up to the row, and a model without a price shows a dash.
- Spend projection (#483). A straight run rate over the last 7, 14 or 30 days projected to the end of the month or 30 days ahead, with the window and assumptions shown, and "Not enough history" when there are fewer than 7 days.
scripts/podium-seed-history.tsfills a throwaway database with past days so the simulator can show the reports and the projection.
Standards: the Admin menu gains a Reporting group and the Server page "Reporting and security" is renamed Security, and new classesexp,note-line,partial,chart,rowbtn,klink,hlinksandcutscome with the page, approved with the mockup on #484 (mockup/podium-admin-reporting.html).
-
The Admin can keep the agent Activity feed, and manage what is stored (#432). Under Admin > Server > Data a new Activity history setting chooses how long an Activity window's entries are kept: Not stored (the default, exactly as before), purge after a number of hours, days, weeks or months, purge when the session ends, or keep everything, with an optional size cap that removes the oldest first. A keep policy needs a token on the server. Entries go to a second SQLite file,
activity.sqlite, beside the database; while a keep policy is on, every running loop of a session whose reporter has theactivityFeedoption on is captured whether or not anyone watches (the mod is unchanged). A purge runs every 5 minutes and writes one Admin log entry per run that removed something.- Stored activity (new Admin page). Totals (entries, size on disk, sessions), filters (session, loop, project, owner, age, a range of dates) and a table of sessions. Delete the ticked sessions, everything a filter matches, or everything; each asks first and says how many entries and how much space will go, then compacts the file so the space comes back. Each delete is written to the Admin log with counts and sizes, never text.
- The Activity window shows stored history, including for an ended session: the newest stored entries with the live ones after them, Show older to page back, and footers that say how many entries are stored, since when, and how long they stay. Only the session's owner and the Admin can read stored text (
GET /api/fleet/respond/sessions/:id/activity-history); everyone else sees who can. Under the default policy the window is unchanged. - Saving a policy that removes stored entries or stops keeping history asks first and says how much will go.
scripts/podium-seed-activity.tsfills a throwaway activity file for load tests.- It is not a complete record: the mod keeps 20 entries per loop and one report carries 60,000 characters, so a busy session can lose entries.
Standards: new.resultand.formacts(a button row inside the existing confirmation box) classes, the.srowselect width and number-plus-unit row, a native button reset for.klinkand.ptab td.who .hint, two new Admin pages (DataPage,StoredActivityTab) and the menu change (Data moves from Reporting to Server and Stored activity joins it, so Reporting has six pages), theOlderRow"Show older" row in the Activity window's status footer (.feedfoot.stat.older), a wider time column when an entry is an hour or more into its loop (.feed.hours), and the stored-history states of the window (the footer count, the note under it, and the empty text for a viewer who may not read it), and the sign-in dialog's wording when stored history is its only reason, approved with the mockup on #432 (mockup/podium-activity-history.html).
-
A Discord server for Podium (#457). The README has a Community section with the invite link, and
docs/COMMUNITY.mdsays how to join, what belongs in Discord and what does not (security problems go to a private GitHub Security Advisory), how the maintainer sets the server up securely and renews the invite, which expires until Discord offers a permanent one.SECURITY.mdand the Code of conduct now name the Discord server. -
The wall tells you when a newer Podium release is out (#499). Once a day the server asks GitHub for the latest release, and when it is newer than the running server the wall shows a notice at the bottom:
Podium 0.15.0 is available, in a green pill, with the version running and a link to the release notes. The X hides that release for everyone, and a still newer release shows the notice again. A check that fails (offline, rate limited, no answer) shows nothing. Admin > Settings > Feature flags has a Release check switch, on by default; off, the server makes no request to GitHub. Pre-releases and drafts are ignored. -
podiumctlruns the container with Docker as well as Podman. Container mode uses Podman when it is installed and Docker otherwise, for building, running and every other container command, and it is the default mode when either is installed.--engine podman|dockerchooses, and the choice is saved asPODIUMCTL_ENGINEsostop,statusand the boot unit keep using the engine that started the container.statusshows the engine. Under Docker the container can reach this machine ashost.docker.internal, and a new.dockerignorekeeps a Docker build to the same files as a Podman one. -
Pin cards on the Fleet wall (#496). A pin button in each card's first row, just before the model, holds that card's place on the wall while statuses, costs, the sort order and new sessions change around it, until it is unpinned. Several cards can be pinned. Pins are kept in this browser, hold their slot inside a band under Group by, and are dropped when their session ends.
Standards: new.pinbutton and.pinslotspace on the session card, approved with the mockup on #496 (mockup/podium-pin-cards.html). -
Kanban page: collapse the KPI rows (#538). The eight KPI tiles fold into one thin bar and open again from a chevron control, so the cards get the space back. The choice is remembered on its own, apart from the header's and the Fleet page's.
- Standards: a new
.kpi-tabcontrol (a pill on the KPI block's edge when open, a full-width bar when collapsed), built on.tbar-tab. Approved by the maintainer on #538, with the mockupmockup/podium-board-kpi-collapse.html.
- Standards: a new
-
An activity entry can carry a Bash call's whole command and its output (#431, protocol). An entry gains optional
id,command,commandCut,output,outputCut,done,exitCodeandfailedfields, the session gainsactivityOutput, and the report reply gainswatchOutput, the loops a viewer has the feed open on. The command rides theactivityFeedoption (up to 20,000 characters, redacted, cut with a visible marker); the output needs the newactivityOutputoption and the Admin's newactivity-outputflag, both off by default, is captured only while a viewer has the feed open, and is never stored. Commands and output have their own 200,000-character budget per report, apart from the 60,000 characters of summaries. An older mod's entries validate and look as before. The server, mod and console changes follow in the same issue. -
The reporter mod sends a Bash call's whole command, and with a new option its output (#431, mod). While
activityFeedis on, a Bash entry carries its callidand its whole command beside the 300-character summary: redacted by the same rule a recording uses, line breaks kept, ANSI codes and other control characters removed, cut at 20,000 characters with a visible[cut off at 20,000 characters]marker. The newactivityOutputoption, "Send a Bash command's output" (off by default, and needingactivityFeed), adds the call's output (stdout then stderr, cut the same way) anddone,failedand, only when Claude Code's text for a failed call names one,exitCode, to the same entry once the call returns. It is read only for a loop the reply'swatchOutputlists, so an older server gets none, and a loop that leaves the list forgets what it held. A report holds at most 200,000 characters of commands and output and at most 480,000 bytes in all, dropping output first and then commands. The report always says whetheractivityOutputis on. -
A finished Bash call says so even when its output was not collected (#598, protocol, mod, server). While
activityFeedis on, the reporter mod sendsdone,failedandexitCodeon a Bash entry whenever the call returns, with or without output; output still needs theactivityOutputoption, theactivity-outputflag and a viewer on the loop, and none of it is kept or sent otherwise. The server holds and serves the three fields with the entry underactivity-feedalone and keeps them when the output goes, and the Activity history still stores only the command. An older mod sendsdoneonly with output, and an older server drops the new use harmlessly. -
A Bash row says whether its call is running, finished or never reported (#598, console). A finished call with no output shows its status mark (a bare check, or a cross with "Exit N, failed" or "Failed") on the line and beside Output, with "Output was not collected." (or the Admin's or the machine's reason, which stay first). The newest call in a loop that is working, with no finish report, says "Running." and "running" on the line. Any other call with no finish report, including an older reporter's rows and every call of a loop that has ended, says it was not collected and shows no mark. The replay is unchanged.
-
A Bash row in the Activity feed opens to the whole command and its output (#431, console). In the agent panel, its Expand modal, the orchestrator's Respond modal and the session replay, a Bash row is closed by default and opens on a click anywhere on it (the line stays a real button for the keyboard). Opened, it shows the command, then the output with a status (a bare check for success, a cross with "Exit N, failed" or just "Failed" when no code is known), each with a Copy button, and a Collapse button. Which rows are open survives the refresh, and opening one does not move the list away from where you are reading. A row with no output says why in words: the Admin has it off, the machine has it off, it is not kept in history, or it was not collected because output is only collected while the panel is open. Admin > Settings gets a warning under the new flag while it is on, and Admin > Server > Data says the history keeps the whole command and never output.
-
Standards: new Activity feed row pieces for an openable Bash row (#431, H3 of #530):
.feed li .tgl(the whole row is the toggle),.feed li .xpwith its.xhheader (the opened command and output panel, wrapped, no sideways scroll),.exit(the exit status: a bare check for exit 0, a cross and the words "Exit N, failed" for a non-zero exit, so the status is a shape, a word and a color), a bare-check success status, a Copy button as a small button inside.xh, and a cross for a failed exit. Two wordings are part of it: "Output was not collected." with the second line "Output is only collected while this panel is open." for a Bash row that finished before the panel opened or whose state is not known, and a cross with the single word "Failed" (no code) for a failed call whose exit code is unknown. Approved by the maintainer with the mockup on #431 (mockup/podium-feed-expand.html). -
Standards: the status mark of an openable Bash row now shows on a row whose output was not collected, on the line and beside Output, and a call still running reads "Running." with the word "running" on the line (#598). Approved by the maintainer with the mockup on #598 (
mockup/podium-feed-expand.html). -
Standards: a new Admin feature-flag row, Bash output in the activity feed (
activity-output, off by default, under Agent activity feed) with a warning bar, and two wordings for a row that has no output, "Output is turned off by the Admin." and "Output is turned off for this machine." (naming theactivityOutputoption, "Send a Bash command's output"). They use the existing.srow,.sw,.warnbarand.feedoff. Approved by the maintainer with the mockup on #431 (mockup/podium-feed-expand-admin.html).
Changed
-
The design standards match the app again (#664).
docs/standards/is regenerated for everything added since 1.2.2: the Certificates, Data, Stored activity and Reporting pages, session recording and its replay, the Plan / Files fold, the certificate pill, the update notice, pinned cards and the opened Bash row. Every component file has a section and every class in the stylesheets is rendered by a sample or listed innot-rendered.jsonwith its reason. -
Reporters connect on their own mutual-TLS port, the console gets no HTTPS, and an expired certificate is not renewed (#665, part of #577). With TLS on, port 4317 stays plain HTTP for the console, Admin and reporters that use a token, and a second port (4318,
PODIUM_TLS_PORT) is HTTPS withrequestCert: trueandrejectUnauthorized: true. It serves only the routes a reporter calls andPOST /api/reporter/renew; a connection with no certificate, an expired one or one from an untrusted CA is dropped in the handshake, and any other path is a404. Nothing in Podium setsrejectUnauthorized: falsenow, which is what CodeQL'sjs/disabling-certificate-validationflagged in the review of #663. The 30-day grace is gone:tls.reporter.graceDays, the expired-certificate branch of the renewal check and its forgery checks, thePAST_GRACErefusal and thegraceDaysfield ofGET /api/admin/peopleare removed (a storedtls.reporter.graceDaysrow is ignored, and a request to set it is a400), and an expired certificate can no longer skip the hourly renewal limit. The reporter helper compares the certificate's end date with its clock before it connects and sends nothing; the status line and/podiumsay "Your Podium certificate expired on 2026-08-30. Ask an Admin to generate a new reporter config." (CERT_EXPIRED, replacingCERT_PAST_GRACEand "renewing certificate").podiumctlanddocs/SERVER.mdcall 4318 the reporter port, and the walk-through no longer tells a person to trust the CA in a browser. SPEC 5.9, 6, 7.1 and 7.7, DECISIONS 42, SERVER, REPORTER, ARCHITECTURE and SECURITY are updated. The Admin pages follow it: Download CA, the browser-trust text and the grace setting are gone, and Admin > People shows a lapses soon badge before a certificate ends. -
The Certificates page says what a reporter that misses the overlap needs (#637, part of #577). While a CA rotation is open, the blue band now says that a reporter that does not connect before the cross certificate ends, or whose own certificate expires first, is refused and its person needs a reissued reporter config, beside the date the cross certificate ends. The Rotate CA… confirmation says the same before the Admin starts, with the number of days and the date the overlap ends, and gives a laptop asleep that long as the example. Wording only, in the existing band and confirmation; the cross certificate's lifetime is unchanged. Mockup:
mockup/podium-tls-certificates.html(#573), state "CA rotating". -
One function signs a person's certificate (#613, part of #577). Making a certificate from a key
opensslgenerates (an Admin issuing a config) and from a public key a reporter sends (a renewal) had two copies of the work folder, extension file, serial, signing and date readback.signClientCertnow does those once and the two callers supply only how the key reachesopenssl x509. No change in behavior; a test now checks both produce the same profile and clean up. -
The left and right panes' collapse buttons are now the pill the top bar, the Kanban KPI rows and the Plan / Files fold use: the same size, hover, focus ring and styled tip, with the shortcut in the tip ("Collapse the left pane ([)") (#626). It sits on the pane's inner edge, near the top, and in the 24 px strip a collapsed pane leaves. Where the panes stack, at 1040 px and below, it is a full-width 28 px bar. All five collapse controls are one component,
EdgeTab, and one set of classes;PaneTaband.pane-tabare gone.
Standards: new.edge-tab, the one collapse and open pill (32 x 16 on a horizontal edge, 16 x 32 on a vertical one, a tip fromdata-tip, a Sonnet focus ring), placed by.edge-tab-top,.edge-tab-kpi,.edge-tab-fold,.edge-tab-leftand.edge-tab-right, and.edge-tab-shutwhile what it controls is hidden. It replaces.pane-tab,.tbar-tab,.kpi-taband.fold-tab, and every later collapse or open control uses it. Approved by the maintainer on #626, with the mockupmockup/podium-pane-collapse-control.html, option A. -
Logs: a refused credential (
auth.failed, the 429 throttle included) and thetls.*events now print in the default text output at warn, where they were hidden unlessPODIUM_LOG_LEVEL=debug. A reporter that retries with the same bad credential gets one line a minute for the same route, source and reason, with a count of the repeats held back. Thejsonformat and shipped logs still carry every line, and no token, certificate or key is printed (#602). -
The simulator (
scripts/podium-sim.ts) sends an OS with each machine of a--person: macOS, Fedora Linux or Windows on the laptop, none for some (an older reporter), and Ubuntu on the build box, so the Machine and System cells show every state (#427). -
Admin > Reporting: the Admin activity report says when the log held more than 1,000 entries in the period and only the newest are shown, and its reply carries
truncated(#529). -
Reporting reads less: a report for today no longer scans the agents, reporter links and failures of every stored session, and the Spend breakdown reads its sessions and agents in two queries instead of several per session (#529).
-
Admin: Data kept is now Data, in the Server group beside Settings, Model prices and Security, because it holds a setting and deletes; Reporting has six pages (#432).
-
Admin: Spend moved from Activity into the new Reporting group, and the Server page "Reporting and security" is now called Security so there is one "Reporting" (#484).
-
Standards: a green variant of the pill,
pill good, joins the default,warnandbad. It is used first by the update notice. Approved by the maintainer on #499, with the mockupsmockup/podium-update-notice.htmlandmockup/podium-update-check-admin.html. -
Added SonarCloud quality and CodeQL status badges to the README (#517).
Fixed
-
A certificate whose person is revoked is refused, and a certificate reissue brings the person back (#659, part of #577).
certAuthand the renewal check now also refuse a live certificate whose person hasrevokedAtset, with the same revoked answer as a revoked certificate, so a path that revokes a token without its certificates cannot leave the reporter in.POST /api/admin/people/:id/certificate/reissueclearsrevokedAt(token unchanged) and logs aperson_restoredentry, so the new certificate works. -
Revoke certificate no longer says the reporter is refused at once (#658, part of #577). The Revoke certificate dialog and the Reporting people help now say it stops the reporter that uses the certificate, that the token still works until it is revoked, and that Revoke token or Revoke all cuts the person off, which is what
requireTokendoes.docs/SERVER.md, the User guide and.github/SECURITY.mdsay the same, and SECURITY.md notes that the Podium CA has no name constraints. -
The CA overlap ends when the cross certificate expires, as the spec says (#657, part of #577).
TrustStore.caCerts()andtrustedIssuers()kept the previous CA trusted for client certificates until an Admin retired it, even aftercross.crthad expired, so a long-lived certificate from the old CA could outlast the 120-day overlap. They now drop the old CA after the cross certificate'snotAfter; retiring it still ends the overlap sooner. The Certificates page's rotation band says the old CA is accepted until that date or until it is retired. This was never a way in: a certificate is accepted only if its fingerprint is a stored row, and nothing new can be signed with the old key. -
The reporter says the clock may be wrong when a good server certificate looks expired or not yet valid (#650, part of #577). A machine with a wrong clock saw
server certificate expiredand blamed the server. Now, onCERT_HAS_EXPIREDorCERT_NOT_YET_VALID, the status line sayscheck the clockand gives this machine's date, in two cases: the server's time is unknown (the handshake failed before any reply), or theDateheader of the last reply from the server differs from the machine's by more than 5 minutes, and then both times are shown. It never says the clock is certainly wrong. A server time within 5 minutes of the machine's keeps the old message.docs/REPORTER.mdhas the new row,docs/SPEC.md7.7 words the rule, anddocs/SPEC.md7.7 anddocs/SERVER.mdnow namePODIUM_TLS_PUBLIC_PORT(#649). -
A reporter config names the HTTPS port people reach the server on, not the container's (#649, part of #577). In container mode
podiumctlalways runs the server on 4318 inside and maps the host'sPODIUM_TLS_PORTto it, so withPODIUM_TLS_PORT=5318every new config saidhttps://<host>:4318, which nothing listens on.podiumctlnow also setsPODIUM_TLS_PUBLIC_PORTto the host port in the container, and the server writes it into a person's config, in the Admin's Create user and in a reporter's renewal. Standalone is unchanged. Configs already issued with the wrong port need reissuing. -
The reporter says where a name that does not match its config file came from (#647, part of #577). A session with no name already took the config file's name; "wrong config for this name" appears only when the session holds another one. The message now says whether that name is from the
userNameoption,PODIUM_USERor the identity the session already holds, and to empty it so the session takes the file's name.docs/SERVER.mdanddocs/REPORTER.mdno longer tell the person to set the name. -
A revoked certificate shows
certificate refused, nottoken refused(#646, part of #577). When a reporter that uses a config file gets a 401, the status line,/podiumand the toast now say the server turned the certificate away and to ask an Admin to reissue the reporter config;token refusedstays for a session with no config file.docs/REPORTER.md's status table drops thetoken refusedrow for a revoked certificate and says whatcertificate refusedcovers. -
The reporter no longer falls back to plain HTTP when
PODIUM_REPORTER_CONFIGnames a file it cannot use (#645, part of #577). A missing file, or one that is not a reporter config, now showsconfig unreadablein the status line and in/podiumwith the path, and the session sends nothing, over HTTPS or HTTP, instead of reporting over the plain address and showingtoken refused. With the variable unset and no file in~/.podium/, nothing changes. -
Admin > People: three follow-ups from the T9 review (#629, part of #577). Download certificate now follows the base Admin is mounted under, so it no longer 404s behind a proxy path. The Actions menu moves focus without scrolling, so opening it cannot scroll the page and close it at once. The Issue new token confirmation says, with TLS on and the person's certificate revoked, that their reporter stays refused until Reissue config; the wording was approved on #629.
-
Sessions renewing together can no longer both get the renewal lock (#611, #616, part of #577). A reporter's renewal lock is now a numbered file,
<config>.lock.<n>, made with O_EXCL and naming its owner. It is held by the highest number and free when there is none or the highest is older than a minute (its owner died, or let go: a finished session ages its file instead of removing it, so a number is never used twice). To take it a session creates the next number, and of several that want the same number exactly one succeeds, so taking over a stale lock removes nothing that two sessions could race on, one can never delete a lock another has just made, and one working from an old reading cannot be handed a number that was held and released since. The older files are removed, leaving the newest. A session that is still running refreshes its file every 20 seconds, so a live renewal is never taken over however long it takes, and one that finishes ages only its own file. This replaces the earlier, unreleased fix for #611, which took a second lock to clear a stale one and could itself race when a process died holding that second lock. The numbered lock also takes the plain<config>.lockof the first renewal code while it works (#621), and a marker,<config>.lock.scheme, records the lock scheme (2) so a helper that finds a higher number refuses to renew with a message instead of ignoring a lock it does not know. So a session on an older helper and one on this helper cannot renew at once either: the older one waits for this one's plain lock, and this one waits for a live plain lock the older one holds. The one race left is with an older helper clearing the same stale plain lock as this one, which only the older code can close. -
npm run devloaded a blank page (#551). The frontend importsprotocol.ts, which importedbuild.tsforparseBuild, andbuild.tsreadsnode:fs; the Vite dev server does not tree-shake that away as the production build does.parseBuildnow lives inparseBuild.tswith no Node imports (build.tsre-exports it), and a test fails ifprotocol.tsreaches anode:module again. -
The Linux start-on-boot notes now say what was tested (#268). On Bazzite 44, standalone mode started at boot with
loginctl enable-lingerand at login without it, across two reboots. Container mode has not been rebooted. Documentation only; no change to the app. -
The Admin > Reporting charts drew every bar as a 5 px dash, because the bars shared the class
barwith the context meter and its CSS height won over the bar's height. The bars now use their own class (rbar) and rise from the baseline to their value, and the chart labels its gridlines (the top value, half of it and zero) and the first and last period (#532). -
The first retention run after an upgrade now writes the day the session cutoff falls in, so the oldest reporting day is no longer missing; it can be partly built (#529).
-
The Markdown linearity test is less sensitive to a contended CI runner: it takes the best of five measurements, allows a ratio below 10, remeasures a case once only if the first ratio exceeds the limit, and cleans up each render outside the timed interval so large DOM trees do not accumulate. A quadratic renderer still measures near 16 and fails (#514).
-
The three SonarCloud issues that held Security and Reliability at C on
mainare fixed in code (#515). The machine id no longer falls back toMath.random()when the sandbox has nocrypto;/podium nameuses a pattern whose parts cannot overlap; and the wall button's fullscreen request no longer sits in atrythat Sonar flagged. The id is still 12 hex characters, and a refused or missing fullscreen still leaves wall mode on and fullscreen off. -
podiumctlprints a secret through its own helper, and its health check only calls the machine's own server (#676).podiumctl tokenandconfig --show-tokenwrite the secret they were asked for withprocess.stdout.write, so no other message can route one intoconsole.log(js/clear-text-logging). The health check calls127.0.0.1for a wildcard bind (0.0.0.0,::or empty, which used to produce an unusablehttp://::address) and brackets an IPv6 bind. It refuses a bind that is neither an IP address nor a plain host name (js/file-access-to-http). Output is unchanged. -
Open CodeQL alerts are resolved in code (#676). A tool name from a hook payload and a model id from the Admin are held in a
Map, so none can reach an inherited property (js/remote-property-injection). The route that servesindex.htmlis limited to 600 page fetches a minute per address (js/missing-rate-limiting). The inline-script finder ends a script at</script >as well as</script>, so a page that uses one still gets the right CSP hashes (js/bad-tag-filter). The twotypescript:S6845warnings on the activity feed and the Facts bar are annotated.