Skip to content

Web UI Reference

Reference for the browser dashboard that finfocus --web serves. For step-by-step tasks, see Use the browser dashboard. For the reasoning behind the security model, see Web UI design.

Terminal window
finfocus --web [--port N] [--no-browser] [--pulumi-json FILE] [--pulumi-state FILE]
[--stack NAME[,NAME]...] [--from DATE] [--to DATE] [--adapter NAME] [--filter EXPR]...
[--allowed-host NAME]... [--trusted-proxy --allowed-user LOGIN...]
  • --web and its companion flags are root-local: they exist only on the root finfocus command and no subcommand inherits them.
  • Every companion flag requires --web. --web and --mcp cannot be combined.
  • With no source flag, the Pulumi project, stack, and state are detected from the working directory, as for finfocus overview.
  • --terraform-state is not accepted.
  • --stack takes one name, or a list for several tabs: --stack dev,prod and --stack dev --stack prod are the same. Surrounding whitespace is trimmed, repeated names count once, and the first name is the first tab. A list of two or more names must be stacks of the Pulumi project in the working directory, and cannot be combined with --pulumi-json or --pulumi-state. One name, or none in a Pulumi project, is the single-stack page: one tab names that stack (the workspace’s selected stack when --stack is absent) and addresses stay #/<view>. --pulumi-json, --pulumi-state, and Terraform show no tab bar.
  • --allowed-host, --trusted-proxy, and --allowed-user serve the dashboard behind an HTTPS proxy on the same machine; see Serve behind a proxy.

The flag table, with defaults, is in the CLI Commands Reference.

Stream Content
stdout One line: the session URL, http://127.0.0.1:<port>/?token=<token>. The token is 128 random bits as 32 hex characters. With --trusted-proxy, a second line holds https://<first allowed host>/.
stderr With --trusted-proxy, a warning that any local process can forge the identity header. Diagnostics and one request log line per request that passes the Host check: method, path without the query string, and status. Tokens and request bodies are never logged.

Unless --no-browser is set, FinFocus opens the URL with the platform opener (open on macOS, rundll32 on Windows, xdg-open elsewhere). A failure to start the opener is logged as a warning; the server keeps running.

Code Meaning
0 The server shut down cleanly after Ctrl+C.
1 Launch failure: no Pulumi project found, port unavailable or outside 0-65535, unknown flag (including --web on a subcommand).
2 Invalid input: a companion flag without --web, --web with --mcp, an unparsable --from/--to, or a bad stack list (an empty or invalid name, a name the project does not have, or a list with --pulumi-json, --pulumi-state, or a Terraform project). The message for an unknown name lists the project’s stacks.

The header links to nine views, and a tenth, Logs, when you turn it on. Each has its own URL fragment, and a Skip to main content link comes first in the tab order.

View Fragment Shows
Overview #/overview Loading checklist; resource table (Resource, Type, Status, Actual MTD, Projected, Delta, Drift%, Recs, Warn); totals; budget health; resource details
Cost #/cost Actual costs, summary with provider shares and carbon equivalencies when plugins supply them, trends when history is available
Anomalies #/anomalies Resources whose month-to-date run-rate has diverged from the projected month: resource, type, period, baseline, observed, deviation, drift %, and currency, with the count of resources considered
Recommendations #/recommendations Recommendation counts, savings, actions, descriptions, and scorer signals when scoring is configured
Estimate #/estimate What-if comparison for one resource: baseline, modified monthly cost, total change, and per-property deltas
Forecast #/forecast Monthly cost projected from the current month, with the growth model or a statistical model fitted to a spend CSV, its bounds, the plan baseline, budget, and warnings
Carbon #/carbon Provider-published monthly carbon emissions for complete UTC months, with source, provider, grouping, period, model version, and retrieval time per record
Config #/config Effective configuration, the tier each value comes from, and edits to known settings in the project or global file
Plugins #/plugins Built-in catalog, installed versions, install, update, remove, validate, and per-plugin settings
Logs #/logs Live log lines from FinFocus and its plugins. Shown only when web logs are enabled.
Control Effect
Filter Filters resources by name or type. Escape in the box clears it.
Sort Cycles the sort field: Cost, Name, Type, Delta.
Run preview Runs pulumi preview on demand and applies pending changes. One preview runs at a time; elapsed time is shown while it runs.
Cluster expand/collapse Expands or collapses a Kubernetes cluster row.
Resource row Opens details: actual and projected breakdowns, current and after-change cost impact, cost drift, property changes, blast radius (dependents with count, monthly total and what the total leaves out; each dependent opens its own detail, with a Back to button), recommendations, budget status.
Previous / Next Pages through large tables.

When a stack mixes currencies, resource rows stay visible and the totals show an explicit totals-unavailable message instead of a sum.

Control Effect
Filter Filters by resource or type.
Group by None, Resource, Type, Provider, Daily, or Monthly. The CLI’s deprecated date alias is not offered.
Tag filter A key=value tag filter, parsed as the CLI parses it.
Sort by Cost, Name, Type, or Delta. For Daily and Monthly, Name becomes Period and Type and Delta are disabled.
Row Opens actual-cost details, breakdown, sustainability metrics, and linked recommendations. Time-period rows have no details.

Results are paged at 250 rows.

Recommendations use the same live inventory as finfocus cost recommendations: preview resources plus unchanged deployed resources, including preview-only creates. A resource present in both state and preview uses the preview version.

Control Effect
Filter Filters by resource, action, or description.
Sort by Savings, Resource, or Action.
Include dismissed Fetches dismissed recommendations too. It reads dismissal records and never changes them.
Row Opens details, including every scorer signal and reasoning.

Dismissed recommendations are hidden by default. The browser cannot dismiss, snooze, or undismiss a recommendation. Results are paged at 250 rows.

The session can return the same lifecycle events as finfocus cost recommendations history for a recommendation in the latest list. The detail panel shows it under Lifecycle: time, action, actor, reason (with the note), and expiry, in the same order as the command. A plugin that could not answer adds a warning above the table. A recommendation with no history, or with no recommendation ID, says so.

The Carbon view calls the same Go method as finfocus carbon actual and shows the same report as --output json. It sends nothing until you press Run report, because each query launches the aws-ce plugin and calls AWS.

Control Effect
Start, End First-of-month UTC dates in YYYY-MM-DD. The end date is exclusive.
Accounting method, Scope Market-based or location-based; total, scope 1, 2, or 3.
Accounts, Regions, Services Optional comma-separated filters.

The total is the sum of the returned available records, calculated by the server. A record with no value shows Missing, which differs from an explicit 0. A report with no records keeps the query you ran and states that the total is unavailable, not zero. A failed query clears earlier results and shows a fixed message; a disabled provider names the setting that enables it.

The Anomalies view calls the same Go function as finfocus cost anomalies and lists the same resources in the same order, for the rows the Overview already holds. It starts no pricing run and has no controls: the list has no filter, sort, or run button. A status line reads N of M resources, or No anomalies found (0 of M resources). when nothing drifted. While the Overview is still pricing, the view shows Waiting for stack data… and loads the list when the session is ready. It refreshes when the Overview does, for example after Run preview. The server formats every amount, percentage, and period. A failed request shows an alert and clears the table.

The Forecast view calls the same Go method as finfocus cost forecast and shows the values of --output json for the same settings, with money and month labels formatted by the server. It sends nothing until you press Run forecast.

Control Effect
Months ahead 1 to 36. The table starts at the current month.
Model Plugin growth (--model growth) or Statistical (--model arima).
Growth type, Growth rate The same as --growth-type and --growth-rate.
Growth cap, Steps The same as --growth-cap and --step; steps are comma-separated DATE=AMOUNT.
Spend CSV, Confidence, Exclude ranges Shown for the statistical model. The browser reads the chosen file and sends its text; the server never opens a path from the page. Ranges are comma-separated FROM:TO.
Include cost history Off is --no-history.

The table has one column per monthly series: the forecast, its lower and upper bounds and the plan baseline for the statistical model, or one column per provider for the growth model. Stored cost history and the spend CSV are summarized below it, with their date range. A failed query clears earlier results and shows the message cost forecast prints for the same settings.

The resource picker uses the same live inventory as finfocus cost estimate, including unchanged deployed resources and preview-only creates. Estimate and Recommendations read the shared snapshot used by Overview.

Control Effect
Resource Chooses the resource. Resources that the initial preview creates are included, also on a first deployment.
Pricing mode Automatic provider, or a mode from a plugin’s pricing spec. Choosing a mode recalculates through that plugin.
Current value Edit a property value, then press Enter or leave the field to recalculate. Leaving a field you did not change does not recalculate.

If the baseline price fails, the property rows and pricing modes still appear, the comparison stays empty, and the error is shown inline. Editing a value or choosing a mode tries the price again. An unknown pricing mode is rejected without those rows.

Control Effect
Edit tier Selects the project or global config.hujson to edit. Project is disabled when no project directory resolves.
Setting editor Changes one known setting. A secret field starts empty; leaving it blank keeps the stored secret.
Remove Drops that key from the selected tier only.
Review changes Asks the server for a before/after preview. The preview writes nothing.
Confirm Writes the previewed changes to the selected file.
Cancel Closes the preview. The file is unchanged.
Reload Reads both configuration files again.

Secrets and notification destinations are shown as set or not set. The page never fills a secret field from the stored value. Alert, routing, and analyzer settings are listed and are not editable. A successful save tells you that the running session keeps its startup configuration.

Control Effect
Catalog row Shows the name, description, providers, capabilities, security level, installed versions, and the latest catalog version. An older install is marked update-available.
Outside the catalog An installed plugin that is not a catalog entry. Validate, settings, and remove stay available. Install and update are not offered.
Install / Update / Remove Opens a confirmation that names the plugin, version, repository, and security level. The download or delete starts after confirmation.
Progress Shows the current phase and, while downloading, bytes received and the total when the host reports one.
Validate Checks the installed files and that the plugin process starts. A failure stays on the page with the reason.
Settings tier Selects the project or global config.hujson, the same files as Config.
Setting editor Edits one plugin setting. A secret field starts empty; leaving it blank keeps the stored secret. When ESC supplies the effective value, the Status cell reads from ESC (<reference>) and the help text names that environment. The page never shows the variable’s value.
Review changes / Confirm Previews, then writes, through the same configuration writer as Config.
Save notice Saved settings apply the next time that plugin is launched.

The page installs catalog names only. A download URL is not accepted. When the newest release has no files yet, the confirmation names the older version that will be installed. A missing checksum file is named before the install continues. A checksum that does not match stops the install.

The Logs view is off by default. When it is off, the header has no Logs link and nothing is captured. GET /api/logs/status still answers 200 with enabled false, so the page can hide the link without a failed request. GET /api/logs/stream does not exist.

Setting Effect
FINFOCUS_WEB_LOGS 1 or true turns the view on; 0 or false turns it off. Any other non-empty value is a validation error (exit 2) when the dashboard starts.
web.logs.enabled true turns the view on in config.hujson. Set it with finfocus config set web.logs.enabled true.

The environment variable wins over the configuration key. With neither set, the view is off. The setting is read when the dashboard starts, so a change applies to the next launch. Capturing starts with the dashboard, so lines from the initial load are already there when you open the view.

Control Effect
Source chips One chip per source seen so far (core and each plugin). Select chips to show only those sources.
Source kind All sources, Core, or Plugins.
Minimum level All levels, or a level and above (trace+ through panic+).
Filter Text to find in the message and in field values. Nothing else is searched.
Regex Reads the filter as a regular expression. An invalid expression is reported next to the box and the previous result stays.
Pause / Resume Stops following the newest line. The status line counts lines that arrive while paused.
Clear Empties the view. The server buffer is not cleared.
Download NDJSON Saves the rows currently shown, after filters, as logs-<timestamp>.ndjson. The browser builds the file; there is no download endpoint.

The status line reads Showing N of M lines. While paused it adds K new lines while paused, with the singular when the count is one. When the view drops rows past its newest 2000, the line says how many older lines were dropped.

Each row shows the time, a level badge that carries the level name as text, the source, and the message. A row that has more fields is a button: Enter or Space expands and collapses them. A row with no fields is not a control. All text is inserted as text, so markup in a message is shown literally.

While the view is following, new lines are announced as they arrive. While it is paused, those lines stay quiet and the status line announces the pending count. If the server buffer no longer has lines from before a reconnect, the view adds a warning such as 3 log lines were dropped from the buffer. That count is lines the buffer already dropped, including ones noticed after a reconnect.

The server keeps the newest 5000 entries in memory. When the buffer is full, the oldest entry is dropped and counted. Nothing the viewer captures is written to disk. The configured log file receives the same lines as it does when the view is off.

Credential-like field names and Pulumi secret values in fields are replaced with [redacted] before an entry leaves the server, and fields whose names start with __ are removed. The free-text message is shown as written.

A plugin’s lines are tagged with its plugin_name field when the line has one. Otherwise the tag is the plugin’s name taken from its binary path, such as aws-public for finfocus-plugin-aws-public. Lines from the host are core. A line in the default text format (logging.format: text) is read for its time, level, message, and fields, the same as a JSON line. Any other line that is not JSON is shown at level info, with the tag of the plugin that printed it. A line that starts with { but is not a complete JSON object, such as one cut at 64 KiB, is replaced by a notice, because its fields cannot be checked for credentials.

While the view is on, FinFocus does not pass FINFOCUS_LOG_FILE to plugins. Their log output goes to standard error, and FinFocus copies it to the log file and to the view. See Web UI design.

The same entries are available in the terminal with finfocus logs.

The Group by selector and the Include dismissed checkbox expose existing CLI options that the terminal UI fixes at launch or lacks. Both go through the same engine code path as the CLI flags.

With two or more stacks the header shows the active stack’s name and a tab bar between the title and the view links. Each tab is one stack, in the order you gave. Every view (Overview, Cost, Recommendations, and the rest) shows only the selected stack’s data; nothing is combined across stacks. The address is #/s/<stack>/<view>; an address with no stack, or an unknown one, opens the first stack.

A stack starts loading the first time its tab is selected, so only the stacks you open run plugins. Leaving a tab ends its live updates, and returning to it shows what it already loaded. A tab shows its state in text:

Marker Meaning
none Not opened yet, loading, or ready
failed The stack could not load. Its view shows the same safe message a single-stack run shows. Other tabs are unaffected. A failed stack stays failed until you restart the dashboard.
locked The stack is waiting for its passphrase. The prompt appears in that tab and unlocks only that stack.

With one stack from a Pulumi project, the header names it and the tab bar shows it as a single selected tab with the same failed and locked markers. Nothing else changes: the address stays #/<view>, and there is nothing to switch to. A plan file, a state file, or Terraform shows no tab bar. On any view but Overview, a stack that is waiting for its passphrase or failed to load says so above the view and links to Overview, with or without a tab.

Input Action
Left / Right on a stack tab Select the previous or next stack, wrapping at the ends
Home / End on a stack tab Select the first or last stack
Tab / Shift+Tab Move between controls
Enter / Space Activate the focused button
Enter in an estimate value, or Tab out of an edited one Recalculate
Escape Close the open detail panel or dialog; in the overview filter box, clear the filter

Closing a detail view returns focus to the control that opened it. Light and dark palettes follow the browser’s prefers-color-scheme setting.

The server binds 127.0.0.1 only. All responses carry Cache-Control: no-store, Referrer-Policy: no-referrer, and Content-Security-Policy: default-src 'self'. Errors are JSON: {"error": "<message>", "code": "<code>"}, except that a missing file under /static/ gets Go’s standard plain-text 404. An unknown path, or an API path called with the wrong method, gets 404 / not_found (unknown path).

Request Status Code Message
GET /?token=<valid> 303 to / - Sets the session cookie
GET /?token=<wrong> 401 unauthorized invalid session token; open the URL printed in the terminal
Any other request without the session cookie 401 unauthorized valid session cookie required; open the URL printed in the terminal
Host other than 127.0.0.1:<port> or localhost:<port> 403 forbidden host header is not this server
Sec-Fetch-Site present and other than same-origin or none 403 forbidden request is not from this server
Origin other than the server’s own 403 forbidden origin is not this server
POST without Origin 403 forbidden origin header required
POST whose Content-Type is not application/json 415 unsupported_media_type Content-Type must be application/json
GET /api/overview/resource?urn=<urn> 200 - Row details plus blastRadius: available, dependents (each with depth, via, status, monthlyCost, priced, component), count, monthlyTotal (omitted when mixedCurrency), currency and leftOut (deleting, unpriced, unresolved). Check available first.
GET /api/overview/anomalies 200 - The same list as finfocus cost anomalies --output json, read from the session’s overview rows and the session stack’s stored daily actuals with no new pricing run: considered and anomalies (resource, type, period, baseline, observed, deviation, percentDrift, severity, method, currency, and baselineWindow on method: robust-median rows). An anomalies item’s method is drift (run-rate divergence) or robust-median (the latest complete day against the median of the resource’s own 14-or-more earlier days). An empty anomalies array with a nonzero considered means nothing qualified. Until enrichment finishes the answer is 503 not_ready. Each item also carries a display object (baseline, observed, deviation, drift, period) and the body a summary count line; the page shows those strings.
GET /api/stacks 200 - The registered stacks in order, {"stacks":[{"name","state"}]}. state is idle, loading, locked, ready, or failed. An empty list with no stacks registered. Root only: it does not exist under /s/<stack>/.
POST /api/stacks/{name}/activate 202 - Starts that stack the first time and answers {"name","state"}. Repeating it changes nothing. Root only.
POST /api/stacks/{name}/activate for a name that is not registered 404 not_found unknown stack
GET /api/config 200 - Effective configuration. Secret values are omitted.
POST /api/config/preview 200 - Before/after diff. Writes nothing.
POST /api/config 200 - Writes the selected config.hujson and returns the page plus a reload notice.
POST /api/config with a stale baseVersion 409 conflict configuration changed since it was loaded; reload and try again
POST /api/config or /api/config/preview with no baseVersion 400 invalid_request baseVersion is required
POST /api/config with the project tier and no project 400 project_unavailable no project directory is available
POST /api/config that fails validation 400 validation The message names the setting. fields lists each failure. The file is unchanged.
GET /api/plugins/catalog 200 - Every built-in catalog entry, plus installed plugins that are outside the catalog.
POST /api/plugins/{name}/plan 200 - Names the version, repository, and security level. Downloads nothing.
POST /api/plugins/{name}/install or /update 202 - Starts the confirmed install. The body matches the fresh plan.
DELETE /api/plugins/{name}/{version} 202 - Starts the confirmed removal of that version.
GET /api/plugins/operations/{id}/stream 200 - Server-sent events: snapshot, then progress, then done.
POST /api/plugins/{name}/validate 200 - {passed, reason}. A failed check is still HTTP 200.
GET /api/plugins/{name}/settings 200 - Plugin settings. Secret values are omitted.
POST /api/plugins/{name}/settings/preview 200 - Before/after diff for that plugin. Writes nothing.
POST /api/plugins/{name}/settings 200 - Writes the selected config.hujson. The message says the settings apply the next time the plugin is launched.
Plugin plan or install for a name outside the catalog 404 not_found plugin is not in the catalog
Install or update whose confirmation does not match a fresh plan 409 conflict The named version, repository, or security level changed.
A second install, update, or remove of the same plugin 409 busy That plugin is already being modified.
Catalog host failure 502 upstream A rate limit says to set GITHUB_TOKEN. Other host failures stay generic.
GET /api/recommendations/history?id=<id> 200 - Lifecycle events for a recommendation in the latest list, oldest first, plus warnings when a plugin could not answer.
GET /api/recommendations/history with no id 400 invalid_query recommendation id required
GET /api/recommendations/history for an id that is not in the latest list 404 not_found recommendation not found
Recommendation history lookup failed 502 history_unavailable recommendation history unavailable
POST /api/forecast/query 200 - The cost forecast --output json document for the session stack, plus display strings. The body takes months, growthType, growthRate (text), noHistory, model, spendHistory (the CSV text, not a path), confidence, growthCap, steps, and exclude (each in the flag’s syntax); omitted fields use the CLI defaults.
POST /api/forecast/query with invalid months, growth, model, spend CSV, or driver 400 invalid_forecast The message cost forecast prints for the same flags.
POST /api/forecast/query when no resource was priced, or priced resources use two currencies 422 no_forecast The message cost forecast prints, such as no cost data available.
POST /api/forecast/query before the session has loaded 503 not_ready forecast data is loading
POST /api/forecast/query whose pricing fails 502 forecast_unavailable forecast data unavailable
GET /api/logs/status 200 - enabled, capacity, retained, dropped, and sources (each with name and kind, core or plugin).
GET /api/logs/status while web logs are off 200 - enabled is false. capacity, retained, and dropped are 0. sources is an empty list.
GET /api/logs/stream 200 - Server-sent events: snapshot (entries, sources, dropped, capacity), then log for each new entry, and gap (missed) when entries were lost between a reconnect and its replay. Each frame’s id is the entry’s seq; send Last-Event-ID to resume. A comment line is sent every 15 seconds.
GET /api/logs/stream while web logs are off 404 not_found unknown path
/api/logs/status with a method other than GET 405 method_not_allowed GET only. This applies whether or not web logs are on.
/api/logs/stream with a method other than GET, while web logs are on 405 method_not_allowed GET only. While web logs are off the route is not registered, so the response is 404 not_found.
GET /api/history/view 200 - The cost history view --output json document for the session stack (stack, snapshots, annotations, currency_warning), plus status: "ok", budget when a global budget is set, and display strings. Optional from and to (YYYY-MM-DD or RFC 3339), currency, and strict (true or false) work like the flags.
GET /api/history/diff?from=<selector> 200 - The cost history diff --output json document, plus status: "ok" and display strings. from and to take v35 or YYYY-MM-DD; an empty to is the newest snapshot. threshold hides changes at or below that monthly amount.
GET /api/history/resource?id=<id> 200 - One resource’s stored series, the points the terminal detail view loads: status: "ok", id, points (timestamp, amount, currency), and display.points.
A history request for a stack that was never collected, a session with no stack, or a diff over a database with no snapshots 200 - {"status": "no_history"}, plus id for /api/history/resource. Run finfocus cost history collect to record history.
A history request with a bad date, reversed range, missing from, negative threshold, unknown or malformed selector, or strict=true over mixed currencies 400 invalid_history_query The message cost history view or diff prints for the same flags.
A history request with a strict value that is not true or false, or a threshold that is not a number 400 invalid_history_query invalid strict value "<value>" (use true or false) or invalid threshold "<value>"
GET /api/history/resource with no id 400 invalid_query resource id required
A history request before the session has loaded 503 not_ready cost history is loading
A history database that cannot be read or belongs to another stack 502 history_unavailable cost history unavailable

Requests under /api/logs/ are not written to the dashboard’s request log, so the stream does not add lines to the log it shows.

The session cookie is finfocus_session_<port>, with HttpOnly, SameSite=Strict, and Path=/, and without Secure. The token stays valid until the server stops.

  • Read-only toward infrastructure. The dashboard does not run pulumi up or pulumi destroy. Plugin queries, the on-demand pulumi preview, and the CLI’s existing optional cost cache and history reads and writes keep their configured defaults.
  • Config can write the resolved project or global config.hujson. The write patches that file in place. Comments, unknown keys, and the other tier’s file stay. The running session does not reload the file.
  • Plugins can install, update, and remove a plugin in the local plugin directory, and can write that plugin’s settings through the same configuration files. Saved settings apply the next time the plugin is launched. The dashboard opens its plugins again after a successful install, update, remove, or settings save.
  • The existing dismissal store is read so earlier decisions are honored.
  • The cost history store is read only. The dashboard does not collect, prune, or export history; run finfocus cost history collect for that.
  • No browser-specific store is added. Missing local stores do not prevent launch.
  • Pages load only from the local server; nothing comes from a CDN.