Blog ·
A shortcut is a reference, not a right
Six cards you can add to a dashboard, and shortcuts that only render when the viewer may still see the target today. Two findings only a real run produced.
By Provibr
The dashboard of a hosting panel is the screen everybody sees first and almost nobody has an opinion about until it is wrong for them. The request was modest: let people add cards, and let them pin shortcuts to a server or an integration they keep coming back to.
Modest requests are where the interesting decisions hide, because a shortcut is a stored piece of user preference that points at a real object with real permissions attached. Getting that relationship wrong is how a panel ends up showing somebody a machine they were removed from three months ago.
Six cards, no migration, and nothing arrives uninvited
There are six addable cards: a list of agents, a list of servers, a list of integrations, a list of tickets, the credit balance, and a shortcut. All six are half width, because they are overviews sitting next to each other rather than anything that needs the full row.
The layout is stored as JSON, and this round did not add a migration. That is a decision rather than an omission. An entry used to be an id and a hidden flag; it now also carries an instance id and a per card configuration. An old row parses cleanly into the new shape, with the instance id filled in from the card id and the configuration left empty, and a test asserts exactly that: an old layout reads back as the same cards in the same order. A migration here would only stamp a timestamp into the journal, and a timestamp in that journal that later turns out to be too high is a real problem for the next round.
The second rule is that an optional card never appears on its own. There is a loop that adds cards a layout does not mention, so that a new fixed card shows up for everybody, and that loop deliberately covers only the fixed cards. If an optional card joined it, "adding" would stop being an action and every dashboard in the product would grow a card nobody asked for at every release.
Which cards were left out is as much a decision as which were included. Monitoring and backups exist in the codebase behind rollout flags that are off, so a card for them would be a card with no users. Recent activity was left out because it already exists as a fixed card.
Two halves of one rule
In a user's stored layout, a shortcut is nothing but a kind and an id. Everything on the screen, the name, the status, the link, is fetched again on every page load with the rights of whoever is looking at that moment. That rule has two halves and both are necessary.
The first is the module. A member who loses the agents module stops seeing their shortcut to an agent, even though it is still in their layout. This gate also knows about surfaces, so on a customer's own branded domain, where billing does not exist at all, the shortcut to the credit balance disappears by construction rather than by a second rule written specially for it.
The second is the object query. The organisation id is in every WHERE, the soft delete filter is next to it, and for a server the member's own server scope is next to that. A server that was never assigned to a member does not exist for that member, and that has to hold for a shortcut they created back when it did.
The shortcut card is also the only card with no module and the only one that may exist more than once. That is correct rather than sloppy: a shortcut is not a screen of its own, it is a reference to a screen that already carries its own permission. Give the card type a module and a viewer with that module could see a shortcut to something they may not open.
Module gone and object gone are different events
Two things that look identical on the screen are treated as opposites.
| What happened | What the viewer sees |
|---|---|
| The module was taken away | The card disappears without a word, and comes back with the module |
| The target was not found | A card saying it no longer exists, with a button to remove it |
The first is silent because the shortcut is not the story. If somebody's role changed, the missing shortcut is a symptom, and announcing it on the dashboard would be noise on top of a change they already know about. It also comes back untouched when the module does, which is only possible because nothing was deleted.
The second is loud because the user did nothing wrong and something was taken away from them. It is deliberately not a 404 and not an error: just a card that names the situation and offers to clean itself up.
Tolerant rendering, not cleanup on delete
The obvious alternative is to remove shortcuts when their target is deleted. We did not, and the reasoning is worth writing down because the opposite choice exists elsewhere in the same codebase: when a member leaves an account, their stored dashboard layout genuinely is forgotten, because that row touches no cascade and would otherwise sit forever in an account they never return to.
Here the balance goes the other way, for three reasons.
Disappearing has more causes than deleting. A member loses a module, their server scope changes, they are looking at a branded domain. No cleanup routine catches those three, so the tolerant rendering path has to exist anyway, and a second mechanism is then only a place that can drift.
The layout is stored per user. One deleted server would mean rewriting the JSON of every member of the organisation, inside a transaction on the delete path. That is write amplification on a warm path in exchange for a screen preference.
And there is nothing to protect. The stored row is a UUID of an object belonging to the reader's own organisation and nothing else. It leaks nothing, it does not grow, and the user can see immediately that it can go.
The end to end test for that is blunt on purpose. After a server is soft deleted, it does not reappear, and its name occurs nowhere in the rendered output. A member without the agents module gets neither an agent shortcut nor an integration one, and the picker does not offer them. An empty server scope yields zero rows rather than everything.
Two findings that only a real run produced
Both of the genuine defects in this round were invisible to unit tests, and both took about ten seconds to find once something real was running.
The picker has a search box, and searching integrations broke the entire picker with a database error. The reason is that the integration type column is a Postgres enum, and there is no case insensitive pattern operator for an enum: the query raised 42883 operator does not exist, and because that query fed the whole picker, nothing at all rendered. Casting the column to text fixes it. No unit test with a stubbed executor would have seen this, because a stub does not have types.
The second came from the accessibility sweep. A shortcut tile is a name, a status and a type, and the whole thing is clickable. The first version put all of that inside one link, and the audit flagged a mismatch between the accessible name and the visible label, because hidden text still counts toward the accessible name. The tile is now a plain container with one link that carries only the name and an empty absolute overlay that catches the click. That introduced a second, smaller problem: the truncation class on the link clipped the overlay to the width of the text, so most of the tile stopped being clickable. Truncation moved to an inner element and the covered area was measured rather than assumed.
What we would write down
- Store a reference, resolve a right. What is in the preference should be an id. What appears on the screen should be fetched with the rights of the person looking, every time.
- A schema-less column is a migration you do not have to write. But only if you can prove the old shape reads back identically, and that is a test.
- Optional means opt in, permanently. A card that appears by itself turns "add" into a word without meaning.
- Distinguish removed from unreachable. They look the same on screen and they deserve opposite treatment: silence for one, an explanation for the other.
- Not cleaning up can be the stronger choice. If the same symptom has causes your cleanup cannot see, the tolerant path must exist regardless, and the cleanup is then only a second thing to keep in sync.
- Run the thing. An enum with no pattern operator and a link that clips its own click area are both trivially visible in a browser, and both invisible to a test suite without a database.
For clarity about what this is: the cards described here are live for every account. Several things are unproven. Reordering by mouse was not driven, the flagged monitoring and backup cards do not exist so their code path was never taken, the branded domain case was checked in a query rather than a browser, and a newly added card only appears after the next server round.
From the platform work on Provibr. What a customer of our customer does and does not see on a branded panel is in white label panels: what the customer sees, and why a summary screen never writes back into the tables it reads is in projecting, never replacing.