Changelog
September 2026
v2026.1.1
- Fixed — a dashboard whose snapshot the server stores as a Parquet file showed empty charts in the browser. Since v2026.1.0 a snapshot above 256 KB of rows is stored as a Parquet file. The browser's DuckDB engine reads that file through its Parquet extension, which it fetches from the app's own origin. The image carried only the JSON extension, so the fetch answered 404. The browser reported that the snapshot data could not be loaded, and every derived view over that datasource failed with a missing-table error. The image now carries both extensions. Nothing to run: reload the dashboard.
v2026.1.0
- Arkush leaves alpha. The landing page, the band under the header and the Help panel no longer warn that the document format changes without migration, because it no longer does: a dashboard saved under an earlier version opens in this one, the format migrates as the dashboard is read, and a change to the state database carries its own numbered step with a copy taken before it. The band now says where saved dashboards live and who can read them, once per machine. An exported copy (Dashboards → Export open dashboard) is still the one copy nobody else holds.
- Operations — versions are now year.release.fix, and this is the first tag under the scheme. The middle number counts feature releases in the year and restarts every January; the last number counts fix-only tags on a release. A fix tag carries fixes only and is safe to take blind: the release check refuses a fix tag on which any setting, format, schema, tool or public path moved. The number says how old an install is and nothing about compatibility — the Operations item on each entry carries that. A renamed or removed setting or tool keeps working until the first release that is both in a later year and at least ninety days after the announcement, and says so at boot or in the tool's description until then. No fix is backported: an install several versions behind moves forward through boot refusals that each name the next step. The
0.xtags stay as they are. - Operations — the state database carries a schema version, and this release stamps it. At boot the service reads SQLite's
user_versionand compares it with its own, which is 1. A database at zero — every install before this release — is read as version 1 and stamped, with no copy and no step. From here on, a release that changes a table names the new version in its Operations item, and before the first pending step the service writes a copy of the database beside it, named for the version it came from. Rollback across a step is that copy; within one version it is the previous image on the same state directory. A database a newer image wrote refuses this image's boot and names the lowest image that can read it. The upgrade and rollback steps are indeploy/README.md→ Versions and upgrades. - Fixed — two writes racing one stored payload name can no longer overwrite each other. The store publishes a snapshot payload or an uploaded file exclusively, so the first writer holds the name and a second writer carrying different bytes is refused, the way a later writer already was. A retry with the same bytes still succeeds.
- A warehouse datasource that names no account says so, everywhere it matters. Since every server-side BigQuery run names a registered service account, a datasource naming none stops running on the server. It now reports itself in the dashboard's findings panel with the fix, its schedule pauses with the reason in the tick's report instead of failing, and the admin page lists every such datasource on the instance under Datasources without an account until the list is empty. The Runs-as picker in the datasource editor also explains an empty list: when the server cannot read an account's policy, or the policy does not name you, the picker shows the sentence and the command that fixes it.
- A dashboard's data moves out of its file. A snapshot's rows used to live inside the dashboard document, which made every save write every row again and made a data-heavy dashboard slow to edit. The rows now live in the snapshot store — one payload per dashboard and datasource, beside the dashboard rather than inside it — and the document carries the payload's name. Nothing changes on screen: the app reads the rows when it opens a dashboard and puts them back when it saves, and a save that only moved a block uploads no data at all. The per-dashboard byte budget is gone with the rows it counted; the 50,000-row cap per snapshot is unchanged. A dashboard you already have moves the first time you save it, with no step from you.
- Operations — the two one-time migration tools are gone, and the image no longer carries them. Every install has cut over to the state plane, and every install has run the data-bar sweep, so neither tool has an input left. The pre-state-plane importer, the eight retired storage variables that refused a boot by name, and the two runbook sections that drove them are all removed. Nothing to do: a deployment that already runs this software runs it the same way. A move from the layout the state plane replaced is now a restore from backup.
- Operations — read this before you update: a misspelled setting now stops the boot. The environment has no spell-check, so
ARKUSH_ADMINin place ofARKUSH_ADMINSused to start normally and grant nobody the admin right. AnyARKUSH_name the service does not read refuses the boot and names the line. Check your env file againstdeploy/README.mdbefore the update rather than after. - Operations — a true or false setting takes the word, and nothing else.
ARKUSH_ALLOW_ANONYMOUS=1used to read as false, so a deployment did the opposite of what its env file said. Writetrueorfalse; anything else refuses the boot. The other three value grammars — lists, byte sizes and durations — are written down in the deploy guide for the first time and are unchanged. - Operations — every `ARKUSH_` setting can be mounted as a file. Point
<NAME>_FILEat a file and the service reads the value out of it, trimming the trailing newline, which is the shape Docker and Kubernetes secrets produce. Setting both keeps the direct value. The settings that already end in_FILEare unaffected. - Operations — the browser's billing project no longer falls back to the server's.
ARKUSH_BROWSER_BQ_PROJECTused to default toARKUSH_BQ_PROJECT. The common configuration therefore served the instance's own project as every signed-in author's default. An author with job-create rights there billed the organization, and one without them read the refusal as the app being broken. SetARKUSH_BROWSER_BQ_PROJECTif your authors should keep a default; unset, each author picks their own project in the app. - Privacy — the kinds of data the software stores changed, and every instance's privacy policy and terms state them. Two rows move and one is new. Dashboard documents no longer hold snapshot rows, and snapshot payloads are now one record per dashboard and datasource — held in the state database below 256 KB of rows and as a Parquet file above it; the same data sits in a different place. The new row is the upgrade copy of the state database: before a schema step, the service writes a copy of the whole database beside it, holding everything the database held, and keeps one such copy at most. Nothing leaves the host. Update the stored-data list on each instance's privacy page to match
docs/security-overview.md→ What is stored. - Operations — the state database stops growing at the old rate, and the file gives its space back only on a rebuild. Each dashboard's rows leave its document on that dashboard's next save, and the version archive stops carrying a copy of them per version. Nothing to run: the change happens as people use the app. On an install whose database is mostly snapshot rows, the freed pages are reused for new data and the file itself stays at the size it reached. To hand that space back to the disk, stop the service and run
VACUUM INTOagainst a new file, then put the new file in place — the same stop the backup already needs. - Agents — nothing changes at your end, and one error is gone.
get_dashboardstill answers a few sample rows per datasource: a snapshot now keeps its own sample beside the column profile, so the tool shows what a row looks like without the rows being in the document. Long strings in the sample are truncated the way profile values already were. Thesnapshot-budgeterror no longer exists —run_datasourceandadd_shared_datasourcecannot answer with it, because the byte budget it named is gone. - Agents and the Source panel — a snapshot's `parquet` field is now `payload`. It names the stored data and the storage picks that data's home by size, so the old name sent a reader looking for a Parquet file that most snapshots never become. The value is unchanged — the same name for the same rows. A dashboard saved before this release is migrated as it is read; nothing to run.
- Composite charts are charts — a spec that facets, repeats or concatenates now lives in an ordinary chart block. The separate
chart_compositeblock type is gone. Nothing changes on screen: a composition still keeps the per-view sizes declared inside it, and still opens the raw-spec editor rather than the encoding form, because every surface now reads the composition off the spec instead of off a stored word. The stored word could disagree with the spec, and an agent writing a faceted spec into a plain chart block produced exactly that: a chart that rendered wrong and reported an advisory instead. That state is now unrepresentable. - A datasource says where its rows come from and whose credential runs them, in two fields instead of one word.
executionanswered three questions at once — the place, the engine, and the identity — souser_oauthandservice_accountwere two words for one BigQuery query under two credentials, and a sheet read by a person's own Google spelled that a third way. Nowsourcesays where the rows come from and carries that source's own address or file, andrunsAssays whose credential the run spends: nothing for the deployment's own, a service-account email for a registered one,userfor the signed-in person's own Google. Nothing changes on screen or on a schedule. The four fields that only meant something next to one word —serviceAccount,file,url,credential— are gone, and the combinations that used to need a warning cannot be written down any more. - Operations — the document format moves to version 5, and no step is needed from you. Dashboards migrate as they are read, in memory, and the next save writes the new shape. There is no bulk rewrite and no pass at boot. A dashboard saved under this release does not open in an older one, which is the usual direction: roll back by restoring the state-directory copy the schema runner writes.
- Operations — every scheduled datasource pauses once, by design. The server's proof that a snapshot came out of an authorized run is bound to the datasource's shape and to who ran it, and both moved in this release, so the proofs written before it no longer verify. Run each credentialed datasource once — from the app, or with
run_datasource— and its schedule resumes. This is the same pause a rotation of the attestation secret causes. - Agents — the block-type vocabulary lost a member.
chart_compositeis no longer a block type, andvalidatenames it as a finding wherever it is written. Write a composition astype: "chart"with facet, repeat or concat in the spec. A document you send at version 1 is migrated for you. The authoring guide and thecreate_dashboard,render_blockandvalidatedescriptions say so. - Agents — `execution` is gone; write `source` and `runsAs`.
{ "execution": "service_account" }becomes{ "source": { "kind": "bigquery" } },"duckdb"becomes{ "kind": "derived" },"local"becomes{ "kind": "imported" }, andfileandurlkeep their names and take their binding inside the source —{ "kind": "file", "entry": "…" },{ "kind": "url", "href": "https://…" }. A named service account moves fromserviceAccounttorunsAs, andcredential: "user"becomesrunsAs: "user". A document you send at an older version is migrated for you. The initialize instructions, the authoring guide and everyvalidatemessage say so. - Google decides who may use a service account. Running a warehouse query under a registered service account used to need the analysts allowlist, or a place on a list kept in the app's own registry file. Both are gone. The app reads the account's own IAM policy in Google Cloud and asks whether that person holds Service Account Token Creator on it — the same rule the account already follows everywhere else in Google. Grant the role to a person and they may use the account here, with no allowlist and no restart. Derived views and file-bound datasources still need no account and no right beyond editing.
- Operations — read this before you update: every registered service account needs a second grant. The service now reads each account's IAM policy, and the impersonation grant it already holds does not carry that permission. Add
roles/iam.serviceAccountViewerfor the service's own runtime identity on every registered account. Until you do, a run naming that account refuses with a message that prints the exact command. No install has this grant today. - Operations — who may run is no longer configured in this application. The
mayRunlist on a registry entry is retired. An entry that still carries one is read as text and grants nobody; the file needs no edit. Grantroles/iam.serviceAccountTokenCreatorin Google Cloud to each person who may use the account. - Operations — an IAM outage longer than an hour pauses server-side runs. The service keeps each policy for one hour after the last successful read. Past that, a run naming a service account refuses and its schedule pauses until Google answers again. Derived views, file-bound datasources and addressed reads are unaffected — none of them asks Google who may spend an account.
- Agents — a service account is Google's to authorize.
run_datasourceon a datasource naming a service account, and the catalog tools browsing as one, now need the Service Account Token Creator role held by the person who approved your grant. The analysts allowlist does not reach a named account. The refusal names the missing role and the command that grants it. The authoring guide says so. - Licence — the warranted document format version moved from 1 to 5. Every signed agreement warrants this number in its annex.
- Licence — the warranted list of destinations gained one host. The service reads a registered service account's IAM policy at
iam.googleapis.com, to ask Google who may use that account. It sends the account's own address and nothing else, it happens only where the service-account registry is set, and an answer is kept at most an hour. Every signed agreement warrants the complete list in its annex. - The instance names itself from inside the app — its display name, its logo, and the security contact
/.well-known/security.txtpublishes now live on the admin page under Instance identity, where an admin edits them without a deploy. They are stored beside the theme library, so a backup carries them and a restore brings them back. - A registry turns on from the app, not from the env file — the groups membership file, the service-account registry, the destination registry and the onboarding questions each sit beside the state plane by default. An admin adds the first group, service account or delivery channel on the admin page and the feature starts working. Previously each one existed only if the operator had first pointed a variable at a path and restarted, while the editor for it sat in the app saying it was not configured.
- Operations — this release renames, removes and adds environment variables. Read this before you update. Every change is one edit to the env file.
- Added, and required:
ARKUSH_IDENTITYstates how the deployment authenticates people —signin(the service signs people in over Google),proxy(an identity front sets an email header the service trusts), ordev(every tokenless caller is one address, localhost only). The service refuses to boot without it. Set the one that matches what the instance already does. Every other identity variable now belongs to exactly one mode, and one set under another mode refuses the boot by name:ARKUSH_SIGNIN_GOOGLE_CLIENT_ID,ARKUSH_SIGNIN_GOOGLE_CLIENT_SECRETandARKUSH_SIGNIN_ALLOWbelong tosignin,ARKUSH_AUTH_HEADER,ARKUSH_LOGIN_URLandARKUSH_LOGOUT_URLtoproxy, andARKUSH_DEV_USERtodev. - Added:
ARKUSH_GROUPSpicks the groups backend —file(the default, the membership file admins edit in the app) orgoogle(the registry of Google group addresses). It replaces the rule that set one of the two file variables and left the other unset. - Renamed:
ARKUSH_OAUTH_CLIENT_ID→ARKUSH_BROWSER_GOOGLE_CLIENT_ID,ARKUSH_GOOGLE_API_KEY→ARKUSH_BROWSER_GOOGLE_API_KEY,ARKUSH_GOOGLE_PROJECT_NUMBER→ARKUSH_BROWSER_GOOGLE_PROJECT_NUMBER, andARKUSH_BQ_BILLING_PROJECT→ARKUSH_BROWSER_BQ_PROJECT. The four are the browser's own Google coordinates, and the old names did not say so — one of them read as a pair with the server's sign-in client, which it never was. - Removed:
ARKUSH_INSTANCE_NAME,ARKUSH_LOGO_URLandARKUSH_SECURITY_CONTACT— set them on the admin page after the update, once.ARKUSH_HANDBOOK_DIRis gone too; nothing set it. - A stale line refuses the boot. Every renamed and removed name above is refused by name at startup, and the refusal says what replaced it. An env file that keeps one does not start the service rather than running with a setting that means nothing — the failure this release exists to remove.
- Changed meaning:
ARKUSH_GROUPS_FILE,ARKUSH_GOOGLE_GROUPS_FILE,ARKUSH_SERVICE_ACCOUNTS_FILE,ARKUSH_DESTINATIONS_FILEandARKUSH_ASK_FILEnow only move a file that already has a place beside the state plane. An instance that sets one keeps working. An instance that sets none gets every registry, empty. A variable that names a path holding no file refuses the boot, because a stated location with nothing at it is a typo rather than an empty registry.
- Added, and required:
- Every server-side warehouse query now runs as a named service account, so Google decides who may spend it. A datasource could leave the account blank and run under the deployment's own identity instead, which reads every dataset that identity was ever granted — and the analysts allowlist handed that whole reach to everyone on it. Blank is no longer a way to run: a datasource names an account an admin registered, and Google's own permissions on that account say who may use it. Derived views, uploaded files and addressed data are untouched, and a query under your own Google still runs in your browser.
- Operations — read this before you update: dashboards whose datasources name no account stop refreshing. Both live instances hold some. For each one, set "Runs as" on the datasource to a registered account and run it once; the schedule resumes from there. Two consequences worth knowing: the analysts allowlist no longer opens the warehouse, so a person who should keep running queries needs the Service Account Token Creator role in Google Cloud instead — and the service's own identity now needs no BigQuery access at all, so you can take that grant off it.
- Agents — a warehouse datasource must name `runsAs`, and the catalog tools require `service_account`.
run_datasourceon abigquerysource with norunsAsanswersforbiddenand names the picker that sets one.list_bigquery_tablesandget_bigquery_tabletake the account as a required argument —list_service_accountsnames the registered ones, and per-account catalogs differ, so the answer is that account's view of the warehouse. Browsing under the deployment's own identity is gone. The authoring guide says so. - A schedule stops when the person behind it loses access. A scheduled refresh runs on somebody's authorization: the person whose run last blessed the datasource. That authorization used to outlive them, so a departed analyst's dashboards kept refreshing under an account they could no longer use themselves. The sweep now re-checks that person before every run, and pauses the datasource when Google no longer grants them the account. Removing someone in Google Cloud is what stops their scheduled spending, and it takes effect within the hour. The sweep's own report lists every datasource it stopped and why, under
unauthorized, naming what the person is missing and, where that is a Google role, the command that grants it back; each refusal also writes a log line naming the document, the datasource and the person. The dashboard itself still reads as scheduled until somebody acts on that report — a screen for it is not built yet. Anyone who may still run the datasource restarts its schedule by refreshing it once. - A snapshot says who refreshed it, and what it ran as. The two used to share one field, so a refresh under a service account showed the account's address where a person's name belongs — and left nobody to check when the schedule ran itself. Now the provenance line names the person, and the snapshot records the account beside it — which is what the server's refresh notice reports back and what agents read. A dashboard saved before this release is migrated as it is read.
- Agents — a snapshot answers `runsAs` beside `refreshedBy`.
get_dashboardandrun_datasourcereport the person the run was made by inrefreshedBy— your grant's consenting account, where you ran it — and the credential it spent inrunsAs, present only when the run named one. Previously a service-account run reported the account inrefreshedByand reported the person nowhere. - Operations — the stack names the image it runs, and every install sets one more line. The base compose file used to build the software from a source tree, which an install does not have. It now runs the image
ARKUSH_IMAGEnames, and compose refuses to start when that line is missing. Put the image and its version tag in the env file:ARKUSH_IMAGE=<image>:v<version>. Pin the version tag rather thanlatest, which moves to another version at the next recreate. The image carries the compose file itself, so an install that holds the image and the deploy guide holds everything it needs to start. To build from a source tree instead, add-f deploy/build.override.ymlto the compose command.
v0.25.0
- Scheduled refresh runs on its own — the service sweeps its schedules every five minutes, with no cron and no scheduler job to set up. The first sweep runs one minute after boot, and the next one starts only after the previous one finishes, so two sweeps never run at once. The same sweep sends due deliveries and subscriptions and purges dashboards trashed more than thirty days ago. On an install that never wired a tick, all of these run for the first time after this upgrade.
- Operations — `ARKUSH_REFRESH_INTERVAL_MINUTES` sets the cadence. Unset means five.
0oroffturns the timer off, for an install that keeps the cadence outside the process.ARKUSH_REFRESH_TOKENstill enablesPOST /refresh/tickfor a sweep on demand; unset, the route answers 404 and the timer runs regardless. A scheduler job or cron that fires the tick can be retired. Every sweep logs onerefresh_tickline withvia(timerorroute) and its duration. A timer tick that lands while a manual sweep runs is skipped and logged asrefresh_tick_skipped.
v0.24.0
- Subscribe to a dashboard in your own Slack direct messages — Subscribe in the header, for anyone signed in who can open a dashboard on the server. Pick a cadence and a time zone, keep the charts and tables you want, and each send arrives from the workspace's bot: the title as a link, when the data was refreshed, the chart images, and the tables in the thread. Nobody has to bless it, because the message goes to you and to nobody else. Send now sends one at once, and the account menu's Subscriptions lists yours. Before every send the server checks that you can still open the dashboard, and pauses the subscription with the reason when you cannot. If your sign-in address is not your Slack address, paste your Slack member ID in the panel. Scheduled deliveries into a channel keep their own rules.
- Share or manage many dashboards at once — Select on the dashboards page reveals a checkbox per row and a batch bar: Share…, Remove access…, Who can view, and under More the tag, transfer, and trash acts. Each act writes one dashboard at a time and reports what landed and what refused. The refused rows stay selected behind Retry. The sharing form's add field takes a pasted list of addresses and groups on one confirm. A group this deployment does not define is refused by name instead of written as a share that grants nobody.
- Admins see every dashboard — a Scope switch on the dashboards page lists every dashboard on the instance. A dashboard not shared with the admin is a metadata row: title, owner, access, tags, and dates, never the content. On such a row the Select mode offers the access acts alone. An admin can close any dashboard to private, and never open one. On the Users page, Transfer all moves every dashboard a person owns and Remove all shares ends every direct share to them, one confirm each.
- The Dashboard settings panel has no dropdowns — the maturity stamp is a row of four segments, each a glyph and a word. A segment click opens a dialog with the note field, so a stamp stays a deliberate act. Official stays in the row for a non-analyst, greyed, with the reason. The sharing form lives in the Share panel, which the header's Share opens, and the settings panel shows the access summary with a Share button in its place. On a share row the role is a View/Edit pair, Add person and Add group are one field, and the version history folds under the newest save and shows Restore on hover.
- The dashboard and datasource lists open fast on a large instance — every list read and parsed every stored dashboard, so an instance with many large dashboards waited seconds for the list page, and every other request queued behind it. The save now writes a summary beside the dashboard, and every list on every surface, agents included, reads the summary. Lists that took seconds answer in milliseconds.
- Operations — the first boot after this upgrade writes the summary for every stored dashboard. One pass before the first request, and one
projection_backfilllog event with the count and the time. An instance with sixty large dashboards spends a few seconds on it. Expected, not a fault. A rollback to an older release keeps working, because the older release ignores the two new columns. A dashboard saved or trashed under the older release keeps the summary it had, and the boot pass on the return skips it, so it lists as it stood before that act. Before the return, with the service stopped, setprojection_versionto null on every row ofdocumentsinstate.db: the boot pass then re-reads every dashboard. - Operations — subscriptions need two more Slack scopes. Add
users:read.emailandim:writeto the bot the destination registry names, reinstall the app in the workspace, and make sure that the token in the registry is the one Slack shows after the reinstall. A registry with one Slack entry needs nothing else. A registry with several Slack entries marks one withdm: true, the bot that opens the direct messages. Until the scopes are there, a subscribe answers with an error that names the missing scope. Where the server has no direct-message bot, the Subscriptions panel names what an admin configures and offers no control.
August 2026
v0.23.0
- Agents get a guide they can hold —
get_authoring_guidereturned one long manual, and an agent that cannot hold a manual searches it for what it already expects to need. It answers with the core now — the document shape, every block shape, the workflow — and then an index naming every other part and what that part covers, so an agent meets a capability it would never have thought to look up. A part is fetched by name, and the whole manual is still one call away. - An agent can see a chart on any filter value —
render_blocktakes the param values to draw under, spelled the way a shared link spells them. Seeing a non-default state used to mean changing the dashboard's own default, rendering, and changing it back: two writes to look at something. Nothing is written now, and an unknown param name or a value the param cannot take is refused instead of quietly drawing the default. - Three things a dashboard can get wrong now say so — a chart carrying its own picker that also opts out of every dashboard filter, where the picker stops working and only the rendered picture showed it. More than one BigQuery datasource reading one warehouse table, where each ran at its own moment and their numbers can disagree with nothing in the dashboard to show it. And a scorecard whose headline is a measure compared against a plain column, which draws no delta at all.
- Two warnings stopped misleading — the minimum-size warning pointed at a block's width when the height was the short one, sending the reader to edit the field that was already fine. And a datasource whose query had changed since its last run reported its own new columns as unknown, a false alarm standing beside the true warning that the data was stale. Column checks wait for the refresh now.
- A number bound to a measure carries no reducer — the guide said a scorecard bound to a measure ignores its reducer while the format demanded one anyway, so authors wrote a value nothing reads. The field is absent now wherever a measure's formula does the arithmetic, on scorecards and pivot values alike. Dashboards written before this release are unaffected.
- Fixed — a save that landed could come back to an agent as a failure. Computing the advisory findings happens after the write and can fail on its own, and that failure was reported as though the patch had never been stored — so an agent would send the same patch again, on top of its own work.
- Operations — nothing to do, and nothing changes in the app. This release is for agents authoring over MCP. Alongside the above,
run_datasourcecan be asked for a lean answer without the column profile, which is most of what a re-run costs on a wide table once the profile has been read once. - Operations — publishing a release notifies no install. Cutting the tag builds the image and pushes it to the registry, and stops there. An install adopts a release by bumping the tag it pins, in its own repository; the one install that tracks main pulls a new image on its own timer. Nothing here reaches into a running machine, so a green build proves an image exists and proves nothing about any install — ask the install's own address which version it serves.
v0.22.0
- The rows behind a block are one press away — open a chart or a table fullscreen and a Rows toggle swaps the drawing for the rows it draws from: the snapshot with the block's filters and your own knobs applied. That is the set the Data as CSV download writes, so the picture, the file, and the rows agree on what stands behind the block. The chart adjustments stand down while the rows are up. A note has no rows and carries no toggle.
- A pivot table opens fullscreen as a crosstab — it opened as a flat list of rows before, because the fullscreen view was written before pivot tables shipped and always drew the plain table. Both places pick the body the same way now, and the page grows to the row groups the screen fits.
- A field can be dragged onto a chart again — drag a column onto an encoding channel and it binds. The list you drag from now sits in the Fields group of the Properties panel, beside the canvas. It used to sit in the datasource editor, which moved into the Data workspace — a full-window room that replaces the canvas — so the list and the blocks were never on screen together and the gesture could not be performed at all. The Properties list is read-only: a column's metadata stays in the datasource editor, which owns it.
- Menu items print the keys they answer to — a menu item with a keyboard shortcut shows it, in the menu, next to the item. The canvas teaches its own shortcuts instead of keeping them in the help page.
- BigQuery and addressed data are what an account buys — a visitor with no account gets what runs with no credential: an imported file, pasted cells, and derived views over them. BigQuery under a visitor's own Google, and reading a sheet or a file at an address, now say to sign in. BigQuery used to run, under the visitor's own Google, against a consent screen that admits 100 accounts for the life of the deployment — so one curious click spent one of those forever. An address used to be accepted and then refused after the fact, in a notice promising a server read that a dashboard living in a browser never gets. Nothing changes for a signed-in person.
- Fixed — signing out left the last dashboard on the canvas. Sign out, take Try without an account, and the canvas came back holding the previous session's dashboard with its snapshot rows. A working copy kept in the browser is stamped with the account that wrote it, and it is now cleared at sign-out and at any load where the server answers and names nobody. A browser with no server at all still restores its drafts, because authoring offline is a supported way to use the app. The local dashboard library takes the same stamp, so one person's dashboards are never offered to whoever signs in next. The Help panel's list of what only the Source panel and agents can author names every execution mode again, after drifting to four of the six.
- Operations — one lost resume per browser on this upgrade. Every account-stamped working copy in a browser is cleared at its first account-free load after the upgrade. The server's copy of a saved dashboard is at most one autosave behind, so what goes is the restore, not the work.
- Operations — the service now records how long it takes to answer. It writes one
requestline per answered request, carrying the method, the path, the status, and the wall-clock time from dispatch to the last byte written. The line carries no identity, and the path never carries its query string, so the record files with the operational events and never with the usage events. The liveness probe and the live-update stream write no line. This is what answers whether the server or the browser is the slow part, which nothing on an instance measured before. An instance that charts its own logs addsmethod,path, andstatuscolumns to its events view. - Operations — five paths that could run without end now stop. Server-side SQL carries the same 30-second budget a BigQuery run carries — a derived view, a reduction over a stored file or an address, and an agent's preview alike — and a run past it stops and names what to reduce. A scheduled tick that arrives while a sweep is already running is refused instead of walking every dashboard a second time beside the first. The cache of block slices drops the slice asked for longest ago once it is full, instead of holding every statement a dashboard ever ran. Deriving a stored file's columns is remembered against the snapshot it read. Two writes of stored data landing at the same moment can no longer publish each other's bytes.
v0.21.0
- A share link you cannot open says so, and stays put — before, a dashboard that was not shared with you opened a blank canvas at
/appunder an error toast, which read as being sent somewhere else. The link now keeps its address and shows a page in its place:This dashboard isn't shared with you, with the account you are signed in as, so the person who sent the link knows whom to share with — once they do, the same link opens. A link to a dashboard that does not exist saysDashboard not found. Signed-out visitors keep the sign-in page they had. All three pages wear the landing page's drifting charts. - Source tables on the Datasources page — a third section lists every warehouse table the instance reads: the table, its catalog description, how many datasources read it, and how many dashboards hold one. A row opens to its readers, each saying how many tables its query joins and where it lives. Two dashboards with separate datasources over one table is the duplication the section exists to show. A dashboard's Model tab draws the tables too, as a first layer in their own tint. Nothing new is measured — every BigQuery run already records the tables it read.
- Table data bars become a scale rule — the bar was a cell type, and a negative number drew as an empty track because a cell cannot carry bounds, a baseline, or two colours. A bar is now a format rule beside the heatmap: one scale and one condition per cell, read apart, so an alarm colours the bar instead of deleting its length. The number keeps a slot sized from the column's own extremes, so paging never moves the bars, and a narrow column drops the bar before the number.
- Operations — read this before upgrading an instance whose tables draw bars. The
barcell type is gone from the format, with no shim: a dashboard written before this release keeps its data and its numbers and stops drawing bars until the one-time sweep rewrites its cells into the new rule.node scripts/migrate-bar-cells.mjs --url https://<host> --header '<identity header>'surveys a running instance with no downtime and says whether any dashboard needs it. If any does, stop the service, run the script with--state <dir>, start it again. Validation names every retired cell it finds until then.
v0.20.0
- The libraries get the app's own dropdowns — the filters and the sort control above the dashboard and datasource libraries were the browser's own menus. They are now the dropdown the rest of the app uses, with a search box on long lists. Each dropdown also holds the width of its longest choice, so picking a shorter value no longer shifts the controls beside it.
- Fixed — a dropdown reopened itself on the click that chose a value. Three places had it: the filters above a library, a block filter's value, and a chart's compare-with. On a long list of fixed choices, a
Use "…"row committed typed text that nothing acts on. That row now appears only where the author asked for free entry. A dashboard's row in the library keeps its tags, contact, and stamp — a size limit on the row used to drop them from a dashboard that carried a lot of metadata. A details page marks its own section in the header again. - Operations — read this before upgrading, on an instance that runs an identity proxy in front. The proxy holds a list of paths it passes through without a session. That list was written by hand from the documentation, and
/assetswas missing from every install. The build hashes the stylesheet names, so no document could spell that path. The result: the landing page, the help pages, and the privacy and terms pages served unstyled. The app now owns the list. Print it withnpm run proxy-paths, compare it against your proxy configuration, and update the configuration. Add--format regexor--format oauth2-proxyfor the shape your front reads. A host that runs a published image and keeps no source tree runs the same script out of the image:docker run --rm --entrypoint node <image>:<tag> /app/scripts/proxy-paths.mjs. Add--anonymouswhere the instance setsARKUSH_ALLOW_ANONYMOUS=true. Print the list again after every upgrade, because it grows when the app gains a public page. Keep/oauth/authorizeout of the list: a person approves an agent under their own identity. An instance that runs the app's own sign-in has no such list and needs nothing here.
v0.19.1
- Operations — the migration tool now travels with the image. The state plane's importer ships inside the published image, so a deployment that pins a release tag and keeps no source tree can run the migration the deploy guide documents. Run it with
docker run --rm -v <old-data-dir>:/import-src:ro -v <state-dir>:/state --entrypoint node <image>:<tag> /app/scripts/migrate-to-state-plane.mjs --from /import-src --to /state, naming the image the stack already deploys. Nothing else changes, and an instance already on the state plane needs nothing from this release. - Operations — preserve modification times when you copy the source. The importer reads each dashboard's last-changed date from its file's own modification time. A copy that resets them gives every dashboard the time of the copy, and the library then shows everything as changed at the moment of the migration.
gcloud storage rsyncpreserves them.cpneeds-p. The deploy guide now says so at the step where it matters.
v0.19.0
- Deleting for good now really deletes — permanently removing a dashboard used to leave its old versions readable in the archive, because the tidy-up only ran on the next save and a deleted dashboard has none. A permanent delete now takes the dashboard, every version of it, and its stored data together. Version history is also capped: each dashboard keeps its last 50 versions, and older ones fall away as new ones arrive. That cap is what makes the promise on an instance's privacy page true rather than reworded.
- Someone outside the analyst list can run one dataset — a registered service account can now carry its own list of who may run under it. Give a person or a group that list and they can refresh the datasources that use that one account, without gaining the wider right to query the warehouse under the instance's own credential. The account's IAM grant is still the boundary: it reaches what it was granted and nothing else.
- A query says what it will scan before it runs — where a deployment sets a ceiling, a run that would scan more than the ceiling stops before the scan starts, so it costs nothing. The size estimate comes from the warehouse itself, through the server.
- Ending someone's access ends their browser too — Revoke all on a person's row now signs out every browser signed in as that account in the same act that cuts their agent connections, and answers with both counts. Previously it cut agents only, and taking the address off the sign-in list was assumed to do the rest — which is true only where that list names the address, and not where it admits a whole domain or everyone.
- Fixed — two people uploading at the same moment could push one person's stored bytes past the instance's limit, because the check and the claim were separate steps. They are one step now. A refused upload no longer leaves the space it was going to use unusable.
- Operations — read this before upgrading. Everything the server owns now lives in one SQLite database in one directory, named by
ARKUSH_STATE_DIR. Ten variables retire with the old layout:ARKUSH_DOCS_DIR,ARKUSH_DOCS_BUCKET,ARKUSH_DATASOURCES_DIR,ARKUSH_FILES_DIR,ARKUSH_FONTS_DIR,ARKUSH_OAUTH_DIR,ARKUSH_PROFILES_DIR, andARKUSH_THEMES_DIR. An existing deployment must run the importer before it starts —node scripts/migrate-to-state-plane.mjs --from <old-data-dir> --to <state-dir>, with the service stopped, because the importer reads one snapshot and has no catch-up mode. The service refuses to boot while a retired variable is set, or while an old layout sits beside an empty database, and names the command. The steps, including how a Cloud Storage deployment prepares its source, are indeploy/README.md. - Operations — the state directory must be a real local disk. SQLite's file locks do not work on a network or object-storage mount, and the database corrupts on one. That rules out serverless platforms with no persistent volume, Cloud Run included. Run exactly one container per state directory; a second is refused at boot. Back up by stopping the stack and copying the directory — the running service holds the database exclusively, so nothing else can read it, and stopping is also what keeps the records and the stored data consistent with each other. Test a restore before relying on the backup: one file on one disk replaces what object versioning used to give for free.
- Operations — the warehouse credential. Where a deployment runs server-side queries, the service's own identity is whatever its host can prove: a cloud platform's attached identity where there is one, otherwise a service-account key file mounted read-only and named in
GOOGLE_APPLICATION_CREDENTIALS, kept outside the state directory so a backup carries no credential. The accounts that identity impersonates still hold no key anywhere. A deployment that supplies no credential leavesARKUSH_BQ_PROJECTunset, and server-side runs answer that the warehouse is unavailable while everything that needs no credential keeps working.
v0.18.0
- Flow diagrams — a new block draws where things go. Point it at two columns — where something came from, and where it went — and it draws each stage as a bar with ribbons between them, sized by how much flowed. It works the stages out from the data itself, so three steps or five needs no extra setup, and every node is named on the diagram rather than in a legend. Hover a ribbon for its pair and its total. A flow that loops back on itself is left out of the drawing, and the findings panel says how many were. Like every other chart it goes into a scheduled delivery as a picture.
- Deleting a dashboard can be undone — Delete is now Move to trash, and a trashed dashboard is held 30 days before it goes for good. While it waits it is gone from every list, and opening its link offers Restore in place of the dashboard. A Trash section on the library page lists what you can bring back, each row naming its owner and the date it goes permanently. Restore returns it exactly as it was: shares, visibility, saved data, version history. Admins see everything in the trash, as oversight. There is deliberately no "delete now" — the clock is the only permanence, so a dashboard deleted in haste is recoverable for a month. Dashboards kept in your browser are not covered: those still delete immediately, and the dialog says so. Agents get the same pair — an agent can move a dashboard to the trash and bring it back, and can no longer delete one outright.
- The findings panel reads your charts, not only your setup — it now says when a bar chart's axis does not start at zero, which makes small differences look large; when a line runs across categories that have no order, which draws a trend that is not there; and other choices that make a chart read wrong while nothing is technically broken. They sit beside the findings you already had about broken references and configuration. Every finding also records which check produced it, so an agent working on your dashboard can tell "this reference is broken" from "this is a judgment about the design" — and the guide agents read now tells them to repair the first and weigh the second, instead of obeying both alike.
- A Google Sheet joins a dashboard by its link — paste the sheet's address and the app asks Google for permission to read that one file, not your Drive. The dialog is Google's own file picker, pointed at the sheet you pasted, and the permission is per person and per file.
- Removing someone removes their agents too — on an instance that runs its own sign-in, taking an address off the sign-in list now ends that person's agent connections in the same act instead of leaving live tokens behind. Admins also get a single Revoke all for one account's agent connections, which is the only cut-off on an instance that admits a whole domain. Group membership read from Google now has a staleness limit: through a Google outage the app serves the last known membership for at most a day, and one hour for a group that grants admin rights.
- What the app says about your data — the data banner leads with the part that matters, that a dashboard file carries its data inside it and anyone you share the file with reads that data. The Help panel's storage section now describes the mode your instance actually runs, rather than every mode it could.
- Fixed — a permission revoked while a request was already in flight could still be honoured by that request. Comment writes, refreshes, deletes, and the dashboard listings now re-check the permission against the copy they are about to write, not the copy they read on the way in. A refusal about a private file no longer quotes its title, and a refusal about a dashboard the app cannot read no longer quotes what was inside it.
- Operations — two optional variables carry Google's file picker:
ARKUSH_GOOGLE_API_KEYandARKUSH_GOOGLE_PROJECT_NUMBER, both from the same Google Cloud project as your sign-in client. Without them the sheet-by-link dialog is unavailable and the app says so where it would have appeared; nothing else changes. Nothing pauses on this upgrade. Trashed dashboards keep counting against an instance's stored bytes until their 30 days are up — held bytes are held bytes — and on GCS the bucket's lifecycle rule expires what a purge leaves behind. Removing a person fromARKUSH_SIGNIN_ALLOWnow also ends their agents' access; on an instance behind an identity proxy there is no allowlist to remove them from, so the admin Revoke all is the act to use.
v0.17.0
- The app signs you in itself — a deployment can now run Google sign-in on its own, with no separate sign-in component in front of it. Open a share link with no account and the address stays where it is: the page asks you to sign in where the dashboard would be, and signing in returns you to that exact link with its filters. No page sends you to Google before the app has said what it wants. Sign out is the app's own. An instance fronted by IAP, Cloudflare Access, or oauth2-proxy keeps working as it does today — that shape stays a first-class mode, picked by configuration.
- Your instance publishes its own privacy policy and terms — an admin writes both in the admin page's page editor. They serve at
/privacyand/terms, and the landing page links what exists. The words belong to the operator, never to the project: a privacy policy names who holds the data, and on your instance that is you. A page left blank answers 404 and nothing links to it, because an empty policy discloses less than silence. Where you connect your Google account, the app now states what the connection gives away — read-only, in this browser, as the connected account. - The landing page wears its own product — small charts drift slowly behind the copy: the app's own marks, no axes and no labels, recolored with the theme and stilled for a reader who asks for less motion. A browser that runs no scripts gets the plain page. The page also names the app and says what it is, which is what Google's brand verification reads before it approves an app that asks a person for Google data.
- A schedule re-runs only what the server itself ran — every server-side refresh now carries a stamp that only the server can write, and the scheduled tick reads that stamp before it spends the warehouse credential. A delivery's approval pins the content it sends, so re-pointing an approved block at other data pauses the send instead of posting it. Both paths used to rest on fields the document carries, which anyone who could edit the dashboard could write. After this upgrade every schedule pauses until one refresh runs, and every delivery until its next Send. The Data panel and the findings panel name what is paused, and normal use clears it.
- Ownership holds at the server — only a dashboard's owner or an admin hands it to somebody else, and no save drops the owner. Editors keep sharing and visibility behind the same confirm as before. A dashboard with no owner never reads as public, whatever its visibility says. An agent cannot write a dashboard's delivery record at all.
- The browser remembers per account — the restored working copy and the Google connection now belong to the account that made them, and both drop when the account changes or you sign out. Two consequences on your first load of this version: you reconnect Google once, and a restored working copy from before the upgrade is cleared. Dashboards kept in the browser library are untouched.
- Fixed — behind a proxy that compresses, Cloudflare among them, every save came back as a conflict and the app reported a change on the server that never happened. Both ends now read the version tag the same way. An instance could answer "too many requests" to everything after a few dozen dashboard opens, because a closed tab's live-update stream stayed open behind the proxy. A stream now retires itself and the tab reconnects. In the Data workspace, ⌘S and undo did nothing, and Safari answered ⌘S with its own save-page dialog. That room now saves and undoes, reports where the working copy stands in its own bar, and asks about an unapplied Source edit on the way in instead of dropping it in silence. The first load also carries less, because the DuckDB engine loads lazily again and hashed assets cache for a year as intended.
- Operations — app-owned sign-in is configuration, and both deploy templates now run one container instead of two. Set
ARKUSH_SIGNIN_GOOGLE_CLIENT_IDandARKUSH_SIGNIN_GOOGLE_CLIENT_SECRETfrom a Google OAuth web client whose authorized redirect URI is<origin>/auth/callback/google, state who may sign in inARKUSH_SIGNIN_ALLOW(*,@domain, or full addresses), and drop the oauth2-proxy sidecar with its skip list. The pair set andARKUSH_SIGNIN_ALLOWunset means nobody signs in, by design. Boot refuses by name a half-configured pair, an allowlist without the pair,ARKUSH_LOGIN_URLorARKUSH_LOGOUT_URLorARKUSH_DEV_USERbeside the pair, andARKUSH_DEV_USERon a non-localhostARKUSH_PUBLIC_URL. An instance that stays on header trust changes nothing. Two new optional variables carry the legal pages,ARKUSH_PRIVACY_FILEandARKUSH_TERMS_FILE, defaulted in the image under/data/pages. Document, snapshot, and file responses now sayno-store, so a cache in front of the service never holds one. On Cloud Run, raisecontainerConcurrency— the template pins 250, and each open dashboard holds one streaming request, so the old default of 20 was twenty dashboards. Under app-owned sign-in the app answers/robots.txtwith its own permissive file. oauth2-proxy answers that path itself withDisallow: /, and an override has to sit in front of the proxy. Two behavior changes to know before you pull: the paused schedules and deliveries above, and one Google reconnection per person.
v0.16.0
- Your instance sets the look for both modes — an admin picks a default theme for light and one for dark on the admin page, and a new dashboard wears the one matching its creator's mode. Any saved theme sits in either slot, and the same theme in both gives one look everywhere. An unset slot falls back to the house theme for that mode, so nothing breaks if you set only one. A dashboard your agent creates takes the light default, or the dark one if that is the only slot filled.
- The chart editor draws as you type — the spec editor now renders the chart as the JSON changes, so you see the result without pressing Apply. Apply is still the one commit: the canvas shows a draft until you take it or revert it. A spec that cannot be parsed holds the last good chart, dimmed, with the reason over it. Completions know your columns — where the spec names a field, the editor offers that datasource's own columns with their types, and it stops suggesting
$schema,width,heightandconfig, the four keys the app strips on the way in. - Table columns resize on the table itself — in edit mode, drag the edge of a column header to set that column's width. Double-click the handle, or press Delete on it, to clear the width back to automatic. The handle is its own keyboard path, so a column resizes without a mouse.
- Tables and lists calm down — a small label in a box now means it is interactive or a warning, and a passive fact reads as a glyph and a word instead. Both libraries lead with a stamp gutter, a datasource's kind folds into its name cell, and Data panel rows run to two lines so names stop truncating. Age reads the same way in every list — "3 days ago", never a bare date.
- Fixed — a chart's inherited title showed the bare field name where the chart drew
Sum of sessions; it now shows what the chart prints. A channel sized by a count of records lost every legend control, because the controls waited on a field the channel does not have. A Tooltip checklist on a wide snapshot ran off the screen, and now caps at half the viewport and scrolls. An imported file kept a reference to bytes that could change underneath it, so the register copies them. A number formatted with a bare comma read one way on an axis and another in a table. Moving between the data surfaces dropped the editing session. A stale chip turned grey under the pointer where it should stay amber. - Operations —
ARKUSH_DEFAULT_THEMEis removed. Set the instance's default light and dark themes on the admin page instead, where they live in the theme library beside the entries. Set them once after you pull: an instance that skips this step seeds new dashboards with the house theme rather than yours. The compose file and the Cloud Run template no longer pass the variable, so an instance that still sets it in its own environment file can drop the line.
v0.15.0
- Scorecards are edited on the canvas — select a scorecard and its blocks appear beside it: Value (the field, how it aggregates, the number format, what the color reads), Comparison, Sparkline, and Code for the JSON-only keys. The chart's Comparison setting moves out of the panel into the same kind of block, so the setting has one home across both types. The panel keeps the datasource and Filters. A blank scorecard opens with the field picker in its body, and a field dragged from the Data panel onto Value binds it.
- The sparkline tells the truth at tile size — it now buckets its rows by calendar period (year, quarter, month, week, or day — picked automatically to fit the tile, or fixed by hand) with the same reducer as the headline number, so the little line agrees with the big number above it. Dates draw on a real time axis instead of evenly spaced. The mark can be a line, bars, or an area — bars are the honest mark for period sums.
- Filters learn operators — two new param types: text (an input box) and number (one or two numeric inputs). The author picks the test — contains, starts with, ends with, equals, the comparisons, between, blank, or a regular expression — and a viewer can flip it at view time; the URL carries the choice like any live value. Authors can also type any default instead of picking one from a list.
- A filter can reach every value — the dropdown's search box used to narrow only the first 200 values, so a value beyond them was unreachable by any amount of typing or scrolling. Search now covers the whole distinct set, and a
Use "…"row admits a typed value the list lacks.(Blank)is a row in every list — and on a multiselect it is an ordinary member: "North or blank" is now expressible, and Exclude carries it like any other pick. - Negative filters keep blank rows — "does not contain", exclude, and every other negative test now keep rows with no value in that column. The old reading silently dropped them: a wrong answer, not a missing one. The same rule reaches a table's conditional formatting, so a negative rule now paints blank cells too.
- Findings reach you, not only your agent — a new rail panel lists what validation knows about the open dashboard: problems and advice, one line each, each line linking to its block. The rail icon counts the problems alone, so it reads zero on a healthy dashboard, and a block with problems carries a marker in its header. The panel also states the dashboard's weight — snapshot bytes against the document budget, rows against the cap, blocks, marks drawn, render time — measured from work the canvas already did. Editors only; nothing scans, nothing runs.
- A shared datasource schedules its own refresh — the entry's details page now carries the same Scheduled refresh section a dashboard's Data panel has. The cadence shows in the library's Refreshed column, so a reader sees how old the data is and whether it stays that way.
- Your fonts, on your charts — analysts upload font faces (WOFF2, TTF, OTF) in the theme panel, and a theme names them like any other font. Charts wait for the named faces before first render, so labels never lay out in a fallback and shift. Server-rendered images —
render_block, delivery sends — read the same fonts (TTF and OTF faces; the rasterizer reads no WOFF2). A deleted face falls back to the font stack, never breaks a dashboard. - The handbook ships in the app — the technical reference for how the instance works: ten chapters, served to admins at
/admin/handbookand linked from the admin page. The Help panel's guides complete alongside: five new guides (bringing data in, sharing and access, controls and cross-filtering, building by hand, theming) and fresh screenshots throughout. The viewer's per-chart popover is now named Chart options everywhere. - Phones can close what they open — the side panel never grows wider than the screen and carries its own close button. List tables scroll sideways inside their own band instead of dragging the whole page along. New dashboard and Use in new dashboard wait for a screen that can edit — a phone-size screen opens every dashboard view-only, so a verb whose whole point is editing led nowhere. Duplicate stays: it copies on the server and never leaves the list.
- Fixed — the control form's Type picker offered types the column cannot carry, like a date range over a region column; it now offers what fits and flags a stranded choice. A selected block rises above overlapping blocks while you move or resize it, and returns to its place on deselect. Signed-out sessions stopped firing server requests that could only fail.
- Operations — one new store with a working default: instance fonts live under
ARKUSH_FONTS_DIR(the image defaults it to/data/fonts; compose adds anarkush-fontsvolume, and the Cloud Run template mounts the same state bucket). The handbook serves from the image;ARKUSH_HANDBOOK_DIRrelocates it when the chapters live elsewhere. One behavior change to know before you pull: the negative-filter rule above changes answers on dashboards that exclude or negate over columns holding blank values.
v0.14.0
- Pivot tables — a table can become a crosstab: fields down the left, fields across the top, values in the cells. The table form's Pivot group turns it on and starts you from a working layout. Every cell, subtotal, and total is computed from its own rows, so an average is a real average and a ratio stays a ratio — nothing is ever a sum of sums. A value is a measure, a column with a reducer, or the plain count of rows. Subtotals and grand totals are switches, row groups open to the level you choose, and past 24 columns the rest gather into one Other column that still adds up, with a note saying so. The download still gives you the flat rows, and a scheduled delivery flattens the pivot rather than refusing to send.
- Cells that address things — a column can draw as a link or as a picture. The address is the cell's own value, or a template like
https://tickets.test/{id}that fills in other columns from the same row — so you can link a table whose SQL you never wrote, without adding a column to hold the address. An address on a scheme that is not allowed, or a placeholder with no value in that row, draws as plain text: a dead link is worse than a visible value. - Color scales in a table — beside conditional rules, a Format rule can paint a range. Add scale tints each cell by where its value sits between a low and a high bound — the bounds you give, or the column's own lowest and highest.
- Click a table cell to filter — cross-filtering is no longer a chart-only gesture. Point a table's column at a param and a click writes that cell's value, exactly as clicking a bar does. Link and picture columns never filter: a click there navigates, and one gesture cannot mean two things.
- Your agent can answer comments — an agent shared into a dashboard as a viewer could read your feedback and never reply. It can now add a comment, reply, and delete its own, at view access — commenting is not editing. Resolving a thread stays an edit.
- Your agent can see what this instance offers — three listings (
list_destinations,list_service_accounts,list_files) hand an agent the names your deployment registered, so building a delivery or pointing a datasource at a stored file stops being guesswork. - Sharing stays a person's act — an agent asked to change a dashboard's owner, its shares, or its visibility is refused and pointed at the sharing form. An agent never reached further than the person who approved it, but the app asks a person before every widening, and an agent has no such moment.
- Fixed — a dashboard created by an agent carried no theme at all, where one created in the app takes the instance's default. A comment written by an agent was bounded far more loosely than the same comment written in the app. A validation finding told agents to read a web address that an agent structurally cannot reach.
- A way to report a security problem — an instance can now name a security contact, and it serves that contact at
/.well-known/security.txt, the address a researcher looks for first. Name no contact and nothing is published: an instance that cannot answer a report should not invite one. The policy behind it states the scope, the response times, and the safe harbor. - Videos respect a viewer who never presses play — an embedded Vimeo video now carries the player's do-not-track flag, which is what YouTube's no-cookie address already did for YouTube.
- Operations — one new optional variable,
ARKUSH_SECURITY_CONTACT: an email address or a URL where a security researcher reports a finding. Set it and the instance serves/.well-known/security.txtnaming that contact. Leave it unset and the route answers 404, so an instance publishes a contact only when it has one. One behavior change to know before you pull: if any of your agents wrote a dashboard's access fields over MCP, those patches now come back refused, and that work moves to the sharing form in the app.
v0.13.0
- Share with your organization's Google Groups — a deployment can now resolve groups from your organization's own Google directory instead of a list kept here. Share a dashboard with
group:team@company.com, or name the group in the analysts or admins list, and everyone in it gets the access — including members of groups nested inside it. Membership stays Google's: the app reads it, never writes it, so people join and leave in one place. A change in Google reaches the app within about ten minutes. An admin's job is choosing which groups this instance uses — adding one checks with Google that the address really is a group, and removing one only stops this instance using it, leaving the group itself untouched. - The Datasources page and the data inventory load quickly again — both pages read every dashboard they list, and they were doing it one document at a time. On an instance whose storage sits in another region that cost seconds per page. They now read in parallel, and each read is a single request.
- A group no longer looks like a person when you share — share rows lead with a glyph, one person or several, so a group is obvious at a glance. This matters most with Google Groups, where a group is an email address just like a person's. The admin page's Groups section now names where membership comes from, so it is never a guess whether a list is this instance's to edit.
- Operations — Google Groups is opt-in per instance: set
ARKUSH_GOOGLE_GROUPS_FILEto a file listing the group addresses, and grant the runtime service account the Groups Reader admin role in the Google Workspace Admin Console (docs/deploy-cloudrun.mdhas the step). It replaces the membership file rather than joining it —ARKUSH_GROUPS_FILEandARKUSH_GOOGLE_GROUPS_FILEare mutually exclusive, and setting both refuses to start rather than guess which one decides who has access. Instances setting neither are unaffected by this release.
v0.12.0
- Read a private Google Sheet with your own Google — a sheet address now offers "Read with: your own Google account". The sheet is read in your browser under your own sign-in, so sheets only you can see work without sharing anything with the server. Consent asks once, for the read-only Sheets permission, at the moment you connect a sheet — one sign-in then covers BigQuery and sheets together. Refresh happens when an editor clicks it; the schedule never runs it, and switching the datasource back to the server's credential pauses the schedule until a server-side run blesses the address again. A renamed tab keeps working: the tab in the pasted link is resolved on every read.
- Tags, contact, and the maturity badge arrive in the app — the "coming release" half of v0.11.0's filing feature: edit tags, the contact line, and the maturity stamp in Dashboard settings and on a shared datasource's page; the badge shows on the view-mode header and in the libraries; both library tables gain a Tags column and tag filters — click a chip to narrow the list. A duplicate keeps tags and the contact and drops the stamp: vouching doesn't copy.
- Multi-select filters grew up — options can sort by a metric (proportional bars, values shown), a filter can flip to "everything except", rows offer a hover only, and a search box appears past eight options. Per-value order and colors are editable on any discrete axis or color: put values in order and recolor them, with theme colors until touched.
- Faster chart building — the blank chart shows the type choice in its body; the field picker offers one-click aggregates typed per column (sum or average for numbers, distinct count for categories); and a block's title can live in the header, on the chart, or hidden — downloads now carry the title as a caption.
- Fixed — the multi-select control silently re-sorted hand-authored option orders alphabetically; the Sheets/URL editor labeled its SQL "BigQuery" though the SQL reduces the fetched bytes locally; chip popovers no longer cover the chip rail (they dock in the corner the rails frame); trackpad pinch zoom lost its sluggishness; schedule and delivery editors now ask for sign-in instead of offering configuration that could never fire anonymously.
- Operations — the landing-copy editor saves with zero configuration: the image defaults
ARKUSH_LANDING_FILEto/data/landing/landing.mdand pre-creates the mount point (a deploy without a/datamount saves container-locally until the next start). The browser's Google token cache became scope-aware under a new storage key, so every author reconnects Google once after this upgrade. To offer the own-Google sheet plane, enable the Google Sheets API on the OAuth client's project; the read-only Sheets scope is Google's sensitive tier, so an unverified client shows Google's warning at consent — the same one BigQuery shows today.
v0.11.0
- Draw on the canvas — shapes and connectors join the block palette. A shape is a rectangle or ellipse with a text label, for annotating a dashboard or sketching one; a connector is a line between two blocks that follows them as they move, with an optional label at its midpoint. Sketch a layout as labeled shapes, connect an agent, and ask it to build the real thing: agents now know to read a placeholder's text and geometry, put the real chart in its place, and remove the sketch.
- Sections: named regions that group blocks — drag blocks into a section and they belong to it; drag the section's label and everything inside moves as one. A section never steals a click — clicking inside it always selects the thing under your cursor, and only the label or border selects the section itself. A transparent section groups without adding any ink.
- Filters can scope to a section — a control's "Applies to" list now offers sections beside blocks. The filter follows the section's membership, so dragging a chart in or out changes what it filters, visibly.
- Bring to front, send to back — every block's overflow menu can reorder overlapping blocks; a new section starts behind the blocks it will hold.
- File a dashboard: tags, contact, and a maturity stamp — dashboards and shared datasources carry free keyword tags, a contact line naming who to ask (an email, a channel, a team), and a maturity stamp —
draft,ready,official, orretired— recording who vouched and when. A stamp knows when the content changed after it was made, so an outdated one reads as "made at an earlier version" instead of silently overpromising. None of it gates anything. These live in the document and over MCP today (list_dashboardsfilters by tag; shared-datasource listings carry the whole stamp); the tag editor, badge, and library columns arrive with a coming release. - Agents may file, never vouch — over MCP, tags and the contact are ordinary patch targets, but writing a maturity stamp is refused: vouching for a dashboard stays a person's act in the app, and
officialneeds the analyst right, enforced server-side. - Operations — nothing new to configure: both features run on existing deployment config. A hand-edited YAML store (groups, service accounts) now reloads correctly after the app itself writes it, not only after a hand edit.
v0.10.0
- The Data workspace — a full-window room for a dashboard's data, opened from the Data panel. A rail lists the datasources; the whole snapshot scrolls as a grid whose headers carry each column's type, role, and label; clicking a header opens that column's editor (label, type, format, description) beside its profile facts. The SQL editor lives in a full-width band under the grid, and a Model tab draws the dependency graph — which datasource feeds which, with staleness and block counts on the nodes. The workspace replaces the bottom dock.
- Measures — name a formula once per datasource (
conversion = sum(signups) / sum(visits)) and every scorecard using it aggregates correctly under any filter, where a stored per-row rate would average wrongly. Formulas are written in the Fields section's editor; columns that exist only to feed a measure can be hidden from pickers. - A Google Sheet or a file at a URL is a datasource — point a datasource at a published sheet or a CSV/JSON/Parquet file on the web or in cloud storage, with reduction SQL over the fetched bytes. Refresh re-fetches the address, on schedule too. A public address runs for any editor; one that spends the server's Google credential needs the analyst right.
- Notes read like documents — tables, quotes, fenced code, rules, and nested lists now draw; a note with text opens fullscreen into a reading column with the source beside it, editable live. Existing notes can render slightly differently: a plain line under a list item now reads as that item's continuation, and a standalone picture wants a blank line around it. The landing page can carry pictures and video.
- Drop several files at once — every file dropped on the canvas imports, each table landing a step from the last. A file that needs the import editor stops the run and a notice names how many are left to drop again.
- Fixed — a reload right after a save no longer boots on a stale base and claims a divergence. Comments posted while you view an old version now arrive when you return to current. A server update arriving mid-drag waits for the gesture to end instead of installing under it. The block comment button asks about unapplied edits before switching panels. A delivery whose attached block was deleted now lists it as "(missing)" so it can be unchecked. A filter bound to a column no shown block carries is now named by validation instead of silently filtering nothing.
- Operations — the OAuth authorize endpoint now refuses scopes outside its advertised list (
invalid_scope) instead of echoing anything requested; clients that follow discovery metadata are unaffected — reconnect one agent after upgrading to confirm the handshake. Addressed datasources are off until the deployment names its allowed hosts (ARKUSH_FETCH_HOSTS).
v0.9.0
- Fullscreen a chart or table — open one block at screen size to read it closely, and download it at that size: a fullscreen chart exports a picture several times larger than a canvas block can give. Charts carry a bar of on-screen adjustments — text size, mark weight, and under More, tick density, gridlines, and per-observation markers. The adjustments are about the screen you are at, so they reset on every open and never ride a shared link.
- The management pages read as one — Dashboards, Datasources, and Users are the same table on the same page width. Dashboards gains Owner, Access, Data (how many datasources and how many rows), Refreshed (the age of the oldest snapshot — a dashboard is only as fresh as its stalest data), and Updated. Datasources gains Kind and Owner, and its "used in" count names the dashboards in a tooltip.
- Users is a table of people — one row each, with the rights and groups they hold, how many agents act as their account, how many dashboards they own and can open, how many shared datasources they own, and how much file storage they use. Admins only. Nothing new is stored: every number is read from what already exists.
- Group membership is edited person by person — each member is a chip you remove on its own, the add field takes one address or a whole pasted list, and a member this instance has never seen is flagged as a possible typo. A group also says what it carries when an allowlist names it, so "grants analyst" is visible before you add someone.
- A quieter alpha notice — the disclaimer now bands the top of the app and pushes the page down instead of floating over its heading and tabs. Messages about something that just happened appear as small notes at the top right; a notice about the open dashboard sits above it.
- Connect an agent and About you work everywhere — both open from the account menu on the management pages, not only over an open dashboard.
- Your organization's name beside the product's — a branded instance now reads "Example Analytics | arkush" rather than replacing the product name; a configured logo stands in for the organization's name.
- Every block header is one keyboard stop — a block's actions became a toolbar, so a ten-block dashboard costs ten Tab presses of chrome instead of sixty. Occasional and destructive actions (Duplicate, Edit spec, Delete) moved into the header's overflow menu, with delete below a divider.
- Fixed — a long dashboard name was cut short in the header even with room to spare; it now uses the space the bar actually has. Switching between View and Edit no longer resizes every chart on the canvas. A dashboard's data columns no longer read "no data yet" while that information is still loading.
v0.8.0
- Build a chart on the canvas — select a chart in edit mode and its parts appear around it: the mark, X and Y, color, size, detail, order, and tooltip, each opening one popover. Drag a field from the Data panel onto a block to bind it. Sort now sits on the axis it sorts. The Properties panel keeps the datasource, filters, comparison, and cross-filtering. Moving from the form to the raw spec is always available, and the form returns when the spec fits it again.
- Value labels and reference lines — print values on the marks, or add a line at a fixed value or at the average, median, minimum, or maximum. The chart stays a chart: it still resizes live, and the blocks still edit it. Hand-tuned layers the editor does not recognize keep the edit-as-code face rather than being overwritten.
- Chart types from a grid — a new chart opens its type grid on arrival, so the first choice is the chart you want instead of an empty placeholder.
- Per-chart filters — a block can ignore the dashboard's controls (
ignoreFilters) or carry filters of its own (fixedFilters). - Compare with the previous period — line and area charts can draw the period before, over a named window such as the last 30 days, or following a live date control. The tooltip shows the true earlier date.
- Viewers can adjust a chart — selected chart aspects are adjustable without edit rights. The adjustment rides the link, so a shared URL carries it, and reset returns to the author's dashboard.
- A header with one job — orientation, save state, freshness, mode, and identity. The block palette rides the header in edit mode, zoom and view theme float on the canvas, New moved to the library page, and dark mode moved into the account menu. The datasources page renders as tables, with skeleton rows on first load.
- Fixed — a menu opened from a bar at the bottom of the window opened downward and off the screen. Every popup now measures itself after rendering and flips up when there is no room below.
- Operations — refresh events carry run cost (
ms,bytesProcessed,cacheHit,mem), and each failed run logs its classified reason, so a repeatedly failing datasource can be named. New rule for public instances: setARKUSH_BQ_PROJECTonly whenARKUSH_ANALYSTSis non-empty — an empty analyst list means everyone, so a server-side credential with one open is reachable by any signed-in visitor.
July 2026
v0.7.3
- Operations — the first release published for both amd64 and arm64: a pinned instance can now run on ARM hosts (Graviton included). Take this bump instead of v0.7.2, which has no published image — its build was cancelled when this one superseded it. Release images now cross-build (the heavy work runs natively, emulation only assembles), so tag builds land in minutes instead of half an hour. No new configuration, and nothing user-facing changed since v0.7.2.
v0.7.2
- The landing page in your instance's own words — an admin can now replace the landing page's copy with markdown of their own, edited on the admin page or supplied as a file from the instance's config. Headings, lists, links, bold and code — the same safe subset notes use. The sign-in door, brand line, and disclaimer stay put; leave the text empty and the stock copy returns.
- Notes render their markdown — a note block now shows headings, lists, bold,
code, and links instead of the raw markdown text. Links open in a new tab, so following one never loses your place on the canvas. Comments stay plain text on purpose: a comment quotes its author. - Operations — release images are now built for amd64 and arm64 both, so a pinned instance can run on ARM hosts (Graviton included);
:mainimages stay amd64-only. OptionalARKUSH_LANDING_FILE(defaultdata/landing.md) names the landing-copy file; unset and unedited means the stock landing page. No other new configuration.
v0.7.1
- Delivered dashboards can include tables — a scheduled delivery now sends table blocks beside charts: pick them in the same list, and each arrives as a real Slack table — the block's own columns, labels, number formats, totals row, and sort — posted in the thread under the main message so the channel stays scannable. A delivery table carries the first 25 rows and says so when the table is longer ("first 25 of 483 rows"); anything Slack's own size limits cut off is named the same way. If Slack refuses a table outright, the message still arrives and a note says the table was omitted — a delivery never goes quiet over a table.
- Operations — image-only bump, no new required configuration. To thread tables under a message that carries chart images, give the Slack bot the
files:readscope and reinstall the app; without it those tables arrive as a separate channel message instead.
v0.7.0
- Send a dashboard to Slack on a schedule — a delivery posts a dashboard into a channel on a cron: message text computed from the dashboard's own data (
{query.column}placeholders filled by one-row queries), charts attached as images. Anyone who can edit the dashboard can set one up in the Data panel; it stays paused until an analyst presses Send now — that send is the approval, and the schedule then repeats exactly what was approved. Any later edit pauses it again, and a failed send says so on the dashboard instead of going quiet. Authors pick from channels an admin registered; bot tokens never leave the server. - Excel import, and imports that show their work — the import editor reads
.xlsx/.xlsworkbooks (with a sheet picker), the import zone leads with drag-and-drop, and the preview shows every column as its detected type with failures made concrete: each header carries a strip counting failing and missing values, a line names the exact values that fail, and a failing cell shows its original value struck through with the reason. Ready-made SQL snippets (dedupe, pivot, unpivot, clean labels…) insert into both SQL editors. - The datasource editor gets the full window — editing data opens a dock across the bottom of the window, the way browser devtools do: the form at its usual width on the left, the data — facts, SQL, preview — wide on the right, the canvas still visible above. Drag the divider to size it; closing the editor folds the dock away.
- Operations — deliveries are off until configured:
ARKUSH_DESTINATIONS_FILEnames a YAML registry of channels (a Slack bot token withfiles:writeandchat:write; the bot must be invited to each channel — the deploy README has the steps, and admins edit the registry in the app). Sends ride the existing/refresh/tick; no new endpoint, no other new required configuration. One new pinned dependency (SheetJS, from SheetJS's own registry) loads only when a workbook is imported.
v0.6.0
- Uploaded files stay — a file you upload can now be kept as the original, not only as the rows it produced. The Datasources page ends with your files, each with its own page for sharing, replacing, and seeing which dashboards read it. A dashboard bound to a stored file re-runs its numbers from the file's current bytes on Refresh — in the app or by an agent — and wears a "File changed" badge when the file moved on without it. A file too large to carry inside the dashboard is stored automatically; smaller imports offer keeping the original as a choice. Sharing a dashboard never shares the file behind it: a refresh needs access to the file itself.
- Chart axes and legends, from the form — a chart channel's Axis & scale section now covers the grid, label rotation (horizontal, slanted, vertical), and which side the axis sits on; color, shape, and size gain a legend switch and a position picker. A number or date format set on a field in the Data panel now reaches the chart by itself — the axis formats with the field's format, no spec editing — and a format written in the chart still wins.
- For agents: flat tables first — the authoring guide now teaches the simpler data shape (a few flat tables that charts aggregate at render time) and tells an agent to stop and confirm direction before its first write. The heavy-chart warning now fires only for charts that draw a mark per row.
- Operations — one new stored population: uploaded files live beside dashboards (
ARKUSH_FILES_DIR, defaultdata/files; with a GCS docs bucket, afiles/prefix in it). No new required configuration. OptionalARKUSH_UPLOAD_QUOTA(for example500MB) caps each person's stored bytes; unset means no limit. The admin page shows the value as status.
v0.5.2
- Take a chart with you — a chart block's download menu now offers the picture as well as the data:
Chart as PNG,Chart as SVG,Data as CSV. The picture is the chart exactly as you are looking at it, filters included, painted on the dashboard's own background at twice the screen resolution; the SVG stays sharp at any size, for print or further editing. Every file takes the block's name. Viewers get the menu too, and a block with only its data to give keeps the plain download button it always had. - Long block titles can be read — a title too wide for its block used to end in an ellipsis with no way to see the rest. Point at it and the full title appears; in edit mode the rename hint rides along instead of replacing it.
- For agents: links, not ids — every MCP result that names a dashboard now carries its
urlbeside its id, so an assistant hands you an address you can open instead of a bare identifier you have to look up. The link is your instance's own canonical address. - Operations — image-only bump, no manifest change. Agent-returned dashboard links are built from
ARKUSH_PUBLIC_URL, the same value the MCP sign-in already depends on, so an instance whose agents can connect needs no new configuration.
v0.5.1
- Fixed: instance rights ignored capitalisation — an admin or analyst whose email was listed with a capital letter held none of their rights, silently. The comparison now ignores case, the same way dashboard sharing always has.
- Hardened: the query engine cannot reach the network — the DuckDB engine that runs agent SQL and derived views is now sealed from HTTP as well as from the local filesystem, so a query can neither fetch a remote file nor carry data out in a URL. Read-only sessions also refuse to start a query outright, rather than only hiding the buttons.
v0.5.0
- The date-range control is a period picker — one field opening a preset list ("Today", "Last 30 days", "This month", "Last quarter", each calendar period with a "to date" variant), typed From/To bounds, and a range calendar with full keyboard support. A preset resolves to fixed dates the moment you pick it, so a shared link shows exactly what you saw; an "Exclude today" toggle ends rolling ranges yesterday.
- Select and multiselect controls are proper dropdowns — the multiselect menu stays open while you toggle values, shows a count of what's selected, floats selected values to the top on reopen, and carries a Clear action. Phones keep the native widgets.
- See what a filter reaches — hover or focus a control and the canvas outlines the blocks its filter touches; a warning outline marks a block the filter would silently miss because its data lacks the column. Screen readers get the same fact as text on the control ("Filters 4 of 6 blocks").
- Guard rails against heavy or mis-filtered dashboards — validation now flags a chart that draws or aggregates more than 5,000 snapshot rows (aggregate upstream, in SQL or a derived view) and a filter that covers only some of its blocks. For agents:
render_blockreports how many marks it drew and how long the render took, and the authoring guide teaches the aggregation grain rules with the actual limits. - Shared datasources show "Runs as" — a library entry's page names the service account its query runs as; analysts assign it right there.
- What's new, in the app — the Help panel shows the release notes for the version you run (this list included), rendered from the changelog itself so it can never drift.
v0.4.0
- Dashboards open in view mode — stored dashboards now open read-only for everyone, editors included; Edit is one press in the toolbar. Phones get a forced view mode with one-finger pan over blocks. The protective default is the point: reading a chart no longer risks dragging it.
- Parallel edits merge on save — two people (or a person and an agent) editing different parts of a dashboard no longer collide: saves rebase through a scope merge, and only a genuine same-region collision surfaces a banner. Agent saves over MCP rebase the same way.
- Version history — every save is numbered and listed by day in Properties; any version opens in a read-only preview, and restore is an ordinary save with the change summarized.
- Upload shortcut — drop a file anywhere on the canvas (or use the empty state's Upload button): the datasource and a table block land automatically; one undo removes the whole import.
- Users page (admins) — a new
/userstab: everyone the instance has seen — sign-ins, profile answers, owned dashboards and shared datasources, connected agents — one row per person. Admin help without ambient access: dashboard titles are visible metadata, but opening one takes an explicit self-share that appears in the dashboard's own sharing list, and ownership transfer covers offboarding. Both acts are recorded in version history. - User registry — with a profiles dir configured, each person's first sign-in writes a stub profile, so the instance knows who has arrived even before (or without) the onboarding ask.
- Datasources page: the "In dashboards" inventory — beside the curated library, every datasource living in a dashboard you can view, with where it lives and its lineage ("derived · reads …"); library entries show how many dashboards use them. Reuse still goes through the library only.
- Dependency guard rails — deleting a datasource now names the derived views that read it (and the bound blocks) in the confirm; the datasource editor shows quiet "Reads:" and "Feeds:" lines.
- Operations — image-only bump, no manifest change.
ARKUSH_PROFILES_DIRnow works standalone (previously it refused boot withoutARKUSH_ASK_FILE); set alone it records sign-ins without asking anything. An instance already running the ask pair needs no config change — the registry activates on this image.
v0.3.3
- Fixed: the engine trapped mid-query once it could start — with v0.3.2 the browser engine boots, then hits the next wall: DuckDB autoloads its json extension from
extensions.duckdb.orgat query time (registering snapshot tables usesread_json), the hardenedconnect-srcblocked that fetch, and the engine collapsed into wasm traps (function signature mismatch) — dashboards stuck at "Filtering…". The CSP now admits the extension CDN in both the document and worker-script policies. Take this over v0.3.1/v0.3.2 — the browser engine is whole only here. Self-hosting the extension beside the wasm binaries (dropping the runtime CDN dependency) is the recorded follow-up.
v0.3.2
- Fixed: v0.3.1's engine repair was incomplete — the blob-spawned DuckDB worker could not resolve the engine's path-absolute wasm URL (a blob worker's
locationis ablob:URL, so relative resolution throwsFailed to parse URL). The engine's module and worker URLs are now absolutized before they reach the worker. Take this over v0.3.1 — on v0.3.1 the browser engine fails to start at all.
v0.3.1
- Fixed: the in-browser query engine broke in current Chrome — transforms, imports, derived-view refresh, and externalized-snapshot reads failed with CSP violations. v0.3.0's strict baseline CSP landed on the DuckDB worker's script file, and a worker is governed by its own file's policy, not the page's; Chrome only recently began enforcing this (a staged rollout — the same deployment worked one hour and failed the next). Script assets now carry a worker-safe policy, and the engine spawns through a blob wrapper so it inherits the page policy — the wrapper is what heals browsers that already cached the old asset response (cached a year, immutable, same URL across releases). Update urgency: high on v0.3.0 — the breakage spreads with Chrome's rollout, not with anything an instance deploys.
- Fixed: new agent connections silently failed in Chrome — the OAuth consent page's Allow button appeared dead in Chromium-based browsers: v0.3.0's hardened CSP (
form-action 'self') blocked the redirect that carries the authorization code back to the agent, because Chromium checksform-actionagainst a form submission's redirect chain. The consent page now names the agent's validated redirect origin in its policy. Existing connections were never affected (token refresh skips the consent page); only consents attempted from Chrome on v0.3.0 hit this. - Help pages — agent-first guides at
/help(readable without a session, proxy-skipped), linked from the Help panel. - Operations — pin
containerConcurrencyin the Cloud Run manifest: left unset, the platform assigns a cap that tracks CPU (20 at 1 vCPU total — hour-long live-push SSE streams count as in-flight requests, so one person's open tabs can fill the cap and the edge 429s everything withmaxScale: 1). The template pins 250 and gives the app container 1Gi to match; take the manifest change alongside the image bump. Also: external uptime checks must target/, never/healthz— Google's edge reserves that path on Cloud Run hostnames and 404s it before the service.
v0.3.0
- Agents can see their work — a new MCP tool,
render_block, returns any chart block as a retina PNG rendered exactly as a fresh viewer sees it (theme, field labels, param defaults applied), so an agent can check a chart visually instead of trusting that its spec compiles. - Chart editor round — picker and switcher widgets in the inspector, foldable encoding channels, and wider Vega-Lite coverage in the chart editor.
- Fixed: pointer drift on zoomed charts — tooltips, clicks, and cross-filter brushes landed off-target when the canvas was zoomed.
- Fixed: scrolling a table block — the mouse wheel over a scrollable table now scrolls the table's rows instead of panning the canvas out from under it.
- Operations — the API now serves
/healthz, and the Cloud Run manifest's startup probe targets it instead of a bare TCP check: take the manifest update and the image together — the probe needs the new image. Also in this batch: security headers and a CSP, request timeouts and request IDs, OAuth rate limiting, container memory rebalanced to the app (512Mi app / 256Mi proxy), image publish now gated on typecheck + tests, and instance deploy config has fully left the app repo — org instances deploy from their own repos, pinning release tags.
v0.2.0
- View any dashboard in light or dark — a palette menu in the toolbar shows the open dashboard in either mode regardless of your app setting; per-person and per-session, never saved into the dashboard. Properties gains a mode-mismatch note with a one-click lossless swap between the built-in light/dark theme pair, and chart tooltips follow the effective look.
- Large-file imports without the freeze — imported files now stage inside the analytics engine; the page reads only schema, row counts, and a bounded preview, so big CSVs no longer lock the tab (ceiling: 512 MB per file). Files over the snapshot row cap get a guided SQL reduction step, seeded with a runnable starter query, and the query's result is what gets stored.
- Fixed: numbers and dates corrupted in stored snapshots — values read back from the in-browser engine could land in snapshots as raw internal objects (sums over large integers) or epoch numbers (dates and timestamps); every engine read now casts to clean values.
- Operations — every push to main and every release tag now publishes a container image to GHCR (
:main/:sha-…for tracking,:vX.Y.Z+:latestfor org instances that pin releases). The host-specific compose stack this repository carried is removed: deploy config for one install belongs to that install's own repository.
v0.1.0
The first versioned release — the point where the app's surface stabilized enough to name. Everything below is what v0.1.0 contains, summarized; behavior lives in docs/spec/.
- The canvas — freeform dashboards of charts (Vega-Lite, unit and composite), scorecards, tables, controls, notes, and images; drag/resize with grid snap and alignment guides, marquee selection, inline editing, undo/redo, viewer scale mode, light/dark theming with an org theme library.
- Data — datasources as SQL against BigQuery (your own Google account or the org's server-side credential, including named service accounts by impersonation), DuckDB transforms over saved snapshots, and imported files or pasted cells; typed query parameters; field labels/formats/thresholds; scheduled server-side refresh; snapshots carry warehouse provenance (source tables and column descriptions).
- Sharing and collaboration — per-dashboard owner/viewer/editor access with deployment groups, share links, comments, presence and live updates, version history and restore, an org-curated shared-datasource library with copy-at-use semantics and two-way promotion (share and un-share from the Data panel).
- AI agents — a full MCP surface (dashboards CRUD via JSON-patch, validation, datasource runs, snapshot-backed previews, the shared-datasource library, the BigQuery catalog), OAuth grants approved per person, and the in-app Connect-an-agent panel: endpoint, client recipes, your connected agents with disconnect.
- Management surfaces — the dashboards quick-switcher panel; full pages with URLs for the dashboard library, per-dashboard and per-datasource details, and administration (groups, service accounts with live verification, user profiles from the onboarding ask, every agent grant).
- Onboarding — a viewer-first landing page with generic and org postures rendered per deployment; a data-first empty-canvas start (add data → make a chart → connect an agent, with a sample dashboard to tinker with); anonymous browser-only mode on public instances with sign-in sync.
- Fixed — deployed instances served no icon files: the image build never copied
public/into the bundle (the favicon 404).