Repository navigation
Web UI at /ui, secure-by-default authentication, remote read hints fix - #51
Merged
Merged
Conversation
… 10) TypeScript stays on 5.9: typescript-eslint does not support 7 yet. Drop echarts-for-react (unmaintained, imports an undeclared tslib) for a small wrapper on echarts core with only the line chart, which also halves the echarts chunk. Drop the unused react-query devtools and switch to @vitejs/plugin-react. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Docker build gets a Node stage and the image ships the static files in /usr/share/sensapp/ui. SENSAPP_UI_ENABLED (default true) and SENSAPP_UI_DIR control it; / redirects to /ui/ when it is served. A missing build is a warning, not a startup failure. The files are public, with a CSP, nosniff and no-cache. The frontend is built for the /ui/ base, with a real favicon and Latin-only Roboto (51 assets down to 9). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The UI has no login: tokens come from sensapp generate-token. On a 401 (missing, invalid or expired token) or a 403 (a token without the read scope) a dialog shows the answer of the server and asks for a JWT. The token is sent as a Bearer token, kept in sessionStorage only, and the refused queries are asked again. The header shows who is signed in, with Sign out, which forgets the token and the data. One unwrap() turns the generated client's returned errors into ApiError with the status, instead of four copies of the same check. Authentication errors are not retried. Checked against a live SensApp with JWT: no token, invalid token, write-only token (403), read token, sign out. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…f the server The generated client had drifted: step, aggregation, limit and simplify, the availability and last-sample endpoints and the delete routes were all missing. A test compares the document with the server's and says how to update both (UPDATE_OPENAPI=1, then npm run openapi-ts). The client is regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- The server refuses more than 100 000 raw samples of a series: past about 2.8 hours the chart asks for a round step (at most 2 000 buckets) averaged, only for numeric series (aggregation on a string series is a 500, see ideas/), and says so in the chart header. - The series list says when the server cut it at 256 series. - Small screens: the page scrolls and the panels keep a usable height, the header fits, the date range wraps. - One Loading and one ErrorAlert replace the copies in the two tables, the invalid CSS and the dead useSeriesData hook are gone, the series checkboxes are labelled. - CI uses Node 24 like the image, and the container smoke test checks the UI. - docs/FRONTEND.md, ideas for what was left out. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…aining labels - The series list has Previous/Next on the bookmark of the server instead of stopping at 256 series; the selection is kept from page to page. /series now documents limit and bookmark, the client is regenerated. - Dark mode: the daisyUI dark theme was declared but never selected. It is now, and echarts uses its dark theme and follows the OS. - No more text in the chart header about averaging, and a shorter token dialog. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Auto (the range decides), Raw, or a fixed step, with avg, min, max, sum, count, first or last, like the window period and aggregate function of Influx and the resolution of Prometheus. Both are part of the query key, so the chart asks again when they change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…yles and a log scale - metric, series, range, step, aggregation and style are query parameters, defaults left out, replacing the history entry. A series is named by its uuid, its name, labels and type come from its first sample; the lookups are queries, so a shared link on a server with a token asks for it and resumes. - Booleans are drawn as a 0/1 step line on an axis of their own, read raw. - Line, step, area, stacked and bars, and a log scale, built by a tested function. - The preset of the range shows as pressed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It wrote its data up to the current ten seconds, and failed when that was a multiple of ten minutes (or the ten seconds before): the row of the coarse archive that ends with the last update is complete, and holds the average of its ten minutes, as it should. The end is now in the middle of an interval, and the boundary case has a test of its own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ht answer Prometheus evaluates the query again on the samples of a remote read, so buckets are only a valid answer for the mergeable *_over_time functions with a range that is a whole number of steps. Count, the aggregation operators (sum, avg, ...) and windows shorter than the step answered wrongly (sum(x) gave 36004 instead of 576, count_over_time gave 1 instead of 60); they now read the raw samples. A live test compares 20 queries evaluated by a real Prometheus with the raw samples; the harness now runs Prometheus v3.8.0 (PROMETHEUS_IMAGE overrides it). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…in two formats Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Prometheus 2 is not supported. v3.15.0 (latest) and v3.8.0 pass too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e, bounded metric names The series list already says how many are selected, and a server that is down shows the error of the request. A metric name is truncated at a fair width (the full name on hover), and a name that exists with two types is two rows with their own keys. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A preset such as 1h is re-resolved every minute while the tab is on screen and as soon as it comes back. The chart keeps what it shows while the data of the moved range loads. Typed dates do not move. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
keepPreviousData does nothing with useQueries (a new key is a new observer, the previous commit's test caught it): the chart keeps the last data of each series itself. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…egend The series list has a column per label dimension and the uuid in small, last. The type is a column only when the series of a name are not all of one type (the server groups metrics by name and type). A selected series shows its color, which it keeps for as long as it is selected, and the chart has no legend of its own. A metric of at most 8 series (the palette) is selected whole, once, when it is chosen; strings are left out as they are not drawn. The chart merges a new option instead of replacing everything, so a series comes and goes with an animation and the zoom stays. The colors are the dataviz palette, validated in both themes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…m the logo The header shows the logo (white on a dark page, without the name on a narrow screen), the favicon is the box and the horn of it. The primary color is the navy of the logo, the neutral one the grey of its dish, in a light and a dark theme of our own. The type badges of the metrics were all grey: the server sends Float and String, and the colors were chosen on float and string. The metric names are cut at the width that the other columns leave, so the table does not scroll sideways. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed, what is left Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… by dragging on it The echarts zoom (slider, wheel, toolbox) zoomed into points that were cut for the window before, and nothing said how to get back. The window of the explorer is now the only one: a drag on the chart sets it and the data is read again at the step that fits (raw samples when zoomed in), and the bar below the chart has back, earlier, zoom out and later next to the presets, the dates, the step and the aggregation. The history keeps the windows the user left (not what the clock did to a preset). While the new data loads the chart keeps the old points and a thin bar says it, where a line of text moved the chart. Long series names no longer leave the tooltip. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… said once, colored checkboxes The series are sorted by the first label column, then the next ones, numbers inside values as numbers (node-2 before node-10); a click on a header sorts by it, again reverses it. The server pages by creation, so the sort is of the page on screen. The selector placeholder is made of two labels of the first series. A label that every series has the same (the org and bucket the InfluxDB importer adds on purpose, they are part of the identity of a series) is said once above the list and left out of the names in the chart, which keeps the tooltip short. The checkbox of a selected series has its color instead of a dot beside it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
More than 8 series no longer repeat a color: the first 8 are the validated palette, the 16 after them are the farthest from the ones before under normal and color-blind vision (the dark theme needs lighter steps than the validated band for that). Past 8 a color does not say which line is which, so hovering a row of the list makes its line stand out and fades the others. The header of the list has a checkbox that selects, or unselects, what is on screen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, the importer labels Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…der in both themes The page and the header were capped at 1280 px and the chart was 260 px high whatever the window: they follow the window now. The hint under the chart is gone. daisyUI's outline buttons and badges were drawn in the full text color (white frames on the dark theme): buttons and fields share a border that is a hint of the text color, the buttons that are on are tinted with the primary color, labels are washes without frames, and the scrollbars are thin and quiet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…st opens on the metric The controls of the chart (window, presets, both dates on one line, step, aggregation) and the filters and the counts of the two lists are in the row of their title, which gives the room back to the chart and the lists: the footer of the series list is gone, its count and its pager are in the header. A metric has a radio button like a series has a checkbox, and a page that opens on a metric scrolls the list to it, once. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s, "Data Explorer" in Sora The page had twice the space at its sides than between the cards. The ghost buttons and the quiet ones had two hovers. The tagline of the header says Data Explorer, set in capitals in a display face. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n SDK, curl) The generators behind the Code dialog, as plain functions with their tests. The code loads what the explorer shows: the selected series (uuid, named in a comment), the window (relative to now for a preset in Python), the step and the aggregation, the series a step cannot average read as they are. A token is only ever a placeholder (`SENSAPP_TOKEN`). Names and labels are quoted or kept on one line, so a label cannot add a line of code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Brains Mono on a dark ground The header has a Code button before API Docs. It opens a dialog with two tabs, Python and curl, coloured by highlight.js (core, Python and Bash only), in JetBrains Mono, dark in both themes, with a copy button that also works on a page served by plain http. The dialog, the highlighter and the font load when it is first opened (13.6 kB gzipped, nothing for a page that never opens it). Checked live: the snippets of the dialog were run against a SensApp, with and without JWT. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… selector of the series list The Python snippet starts with an inline script block (`# /// script`) so that `uv run script.py` installs the SDK from GitHub by itself; the `uv pip install` command stays next to it. The selector box of the series list moves to the selection store. When one is typed, the code asks the server for the series that match it (`list_series(metric=, selector=)` in Python, `curl -G --data-urlencode` into `jq` and a loop in the shell) instead of listing uuids, and a checkbox of the dialog goes back to the checked series. A step is only asked for the numbers among series whose types are not known when the code is written. Checked live: `uv run` with only the block, selector snippets against a SensApp (mixed types, with a step), and a hostile selector, metric and address in a real shell with curl stubbed: no command ran. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ing sideways CSS only: pre-wrap, and a token with no space (a uuid, an address) breaks where it must. The copied text is the same. Looked at on desktop and at 375 px, both tabs: nothing scrolls sideways. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eus and curl snippets to copy The header gets two tabs, Data Explorer and Load Data (/ui/load, loaded on demand, the way in ?via=). The snippets take the address of the server, read the token from SENSAPP_TOKEN when it asks for one, and were all run against a real SensApp, with and without a JWT secret. Telegraf sends 'Authorization: Token', SensApp reads 'Bearer': the snippet overrides the header, and ideas/influxdb-token-authorization-scheme.md notes the server-side fix. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed tabs and the text beside the code Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d says less Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Without SENSAPP_JWT_SECRET, a loopback address now makes a secret for the run and prints a 24 hour token with a UI link, any other address refuses to start, and SENSAPP_AUTH_DISABLED=true is the explicit way to run open. `sensapp generate-secret` prints a random 48 character secret. Breaking: a container or server without a secret must now choose. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n the logs Tokens get a jti, iss and aud (checked), and a kid naming the secret that signed them, so SENSAPP_JWT_PREVIOUS_SECRETS can rotate the secret: the new one signs, the old ones only verify, and dropping one revokes its tokens. The new `admin` scope (mint tokens, nothing else) is never implied by another scope. `Authorization: Token <jwt>` is accepted like Bearer, for InfluxDB clients and Telegraf. The validated subject and token id are recorded in the request log span. The CLI and the future HTTP endpoint share TokenRequest, which checks the subject, scopes, sensors and duration in one place. Breaking: tokens made by hand need iss and aud set to `sensapp`. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Needs the admin scope, which gives nothing else. The endpoint never makes an admin token (those take the secret, through `sensapp generate-token`), caps the duration at SENSAPP_TOKEN_MAX_DURATION_SECONDS (one year by default), answers with Cache-Control: no-store, is a 404 when authentication is disabled, and logs who made which token, without the token. SensApp keeps no token: nothing is listed or revoked, rotating the secret is how. The OpenAPI document of the frontend and its generated client are regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…igns in An admin token gets a form (name, scopes, sensors to click in, duration), then the token is shown once with a Copy button and the export line the Load Data code reads. Without an admin token the tab says how to make one, and says so when the server is open. The catalog is not read with an admin token, which cannot read: its refusal would ask for a token again. The link a local SensApp prints (/ui/#token=...) signs the UI in and the fragment is taken out of the address. A refused mutation opens the sign-in dialog like a refused query does. Telegraf's snippet uses its own `token`, since SensApp reads the Token scheme. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Helm chart makes a random secret on the first install and keeps it across upgrades (auth.jwtSecret, auth.existingSecret, auth.previousSecrets and auth.disabled choose otherwise), and NOTES.txt says how to make an admin token. The demos, the live and perf scripts, the CI smoke job and the quickstart notebook set SENSAPP_AUTH_DISABLED=true explicitly, and a CI step checks that the image refuses to start without authentication. docs/JWT_AUTH.md is rewritten (startup modes, admin scope and endpoint, rotation and revocation, logs); CONFIGURATION, FRONTEND (Credentials tab, token in the link) and the chart README follow. Listing and revoking single tokens is left in ideas/. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… document Sensor names of a token are kept exactly as given (they were trimmed), so a name with a comma or a space around it works through the endpoint, and the new repeatable `sensapp generate-token --sensor <name>` takes one name as it is. `--sensors` still splits on commas. The OpenAPI document declares the bearer token as a security scheme, and each protected operation names its scope (read, write, delete, admin); /prometheus/metrics is public and takes a read token for the latest samples. A test keeps the declarations in step with the routes. The frontend client is regenerated. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mmand line, and a token section in every way The Credentials page loses its card and takes the whole width as Load Data does. The validity comes before the scopes, the sensors are chips (a name is kept as typed, so a comma or a space works), and a Command line section shows the `sensapp generate-token` command of what the form says, quoted for the shell, which also covers what the endpoint cannot do: an admin token. "Make it here" is the button for an admin token, and tells how to get one otherwise. Load Data starts every way with the same "A token" section (where the code finds the token, how to make one, a link to Credentials) when the server asks for a token, instead of comments scattered in the code and a paragraph at the top. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The command suggested for an admin token (sign-in dialog of Credentials, the chart notes, the docs) is now `--scope read,admin`: the admin scope reads nothing, so a token with only it signed in and the explorer asked for another token. The scopes stay separate on the server, and the Credentials page reads the catalog when the token can read. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the others They were the neutral grey of daisyUI, unlike the checkbox of the series table. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the button that makes it The name comes from the words of the names of the containers of Docker. Make it here comes before the command line, and the token is shown below the button, not at the top of the page. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… together, before its details Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rests on the border The tabs now run the full height of the bar so the line under the current one sits on the border of the header, a short divider replaces the full-height one, and Sign in / Sign out are the height of the Code and API Docs buttons. On a narrow screen the tabs scroll instead of widening the page. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…om of the header The tab strip was centered in the bar instead of stretched, so the line floated above the border. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…bled or not, and the arrows are of one width Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
1. A web UI, served by SensApp under
/ui/A React single-page app in
frontend/, built to static files that SensApp serves itself (/redirects to it, the container image ships the build, no separate container).uv, Telegraf, Prometheus, curl) built from the state of the explorer.nosniffandno-cache.SENSAPP_UI_ENABLED/SENSAPP_UI_DIR; a missing build is a warning, never a failure. Seedocs/FRONTEND.md.frontend/openapi.jsonis generated from the server, and a test fails when it is stale.2. Secure-by-default authentication⚠️ breaking
SensApp no longer runs open unless told to.
SENSAPP_JWT_SECRETSENSAPP_AUTH_DISABLED/ui/#token=…link are printedtrueadminscope.POST /api/v1/admin/tokenslets an admin token make tokens (duration capped, never makesadmintokens; only the CLI can).sensapp generate-secret,generate-token --sensorfor names with commas.jti,iss/aud,kid; secret rotation withSENSAPP_JWT_PREVIOUS_SECRETS(which is also how tokens are revoked); the InfluxDBAuthorization: Tokenscheme is accepted; the token subject is in the request logs.existingSecret,previousSecrets,disabled), Dockerfile, compose files, perf/live scripts, the notebook and CI follow the new default. CI gains a step that checks the image refuses to start open.ideas/token-registry-and-revocation.md.3. Remote read hints fix
fix: remote read hints: only aggregate when Prometheus gets the right answer, with the reasoning indocs/anddone/remote-read-hints-semantics.md. The live Prometheus harness now runs v3.15.0, and has a case for it.Housekeeping
Frontend dependencies refreshed (Vite 8, Vitest 5, ESLint 10), CI Node 22 → 24, a flaky clock-dependent test fixed. Task files are in
done/, follow-ups inideas/.Before merging
SENSAPP_JWT_SECRETit listens on localhost only, with a secret made for the run and a token printed at startup; on any other address it refuses to start. SetSENSAPP_JWT_SECRET(sensapp generate-secret), orSENSAPP_AUTH_DISABLED=trueto opt out. See docs/JWT_AUTH.md."SENSAPP_JWT_SECRETorSENSAPP_AUTH_DISABLED=trueon upgrade.Verified
cargo clippy --all-targets --tests,cargo fmt --check: clean.cargo test: 276 unit, 311 integration, 2 doc tests on SQLite; 276 / 301 / 2 on PostgreSQL (--no-default-features --features postgres).eslint,tsc, 261 tests (1 skipped), production build.helm lint/helm templatewith the four auth setups.Not verified here
lookupthat keeps the made secret across upgrades (no cluster available).🤖 Generated with Claude Code