The dashboard is a grid a person arranges by dragging #125

Closed
opened 2026-09-09 10:10:46 +00:00 by tiagoagueda · 2 comments
Owner

Observation

the dashboard, even if inicilized for a new user, must be unique for each user.
the disposition can be flexible in a 4 or 5 x something grid, widgets organized by
drag-drop, for now using builting widget (internal widgets plugin) but later can be
extended (future release)

Third of three. Blocked by the two above: one decides where an arrangement is stored, the
other decides how a widget is placed by somebody not using a mouse.

What exists

Seventeen widgets are registered, seven of them in the default arrangement:

Group Widgets
What needs doing suggestions, counters, gone_quiet, upcoming_interviews, due_reminders, recent_activity, shortcuts, listings
What the record says insight_counters, funnel, outcomes, durations, interviews_summary, sources, industries, by_month, selectivity

Each declares a width of quarter, half or full, and core/dashboard.html turns that
into lg:col-span-3, -6 or -12 over a twelve-column grid, in one flow, ordered by
the stored list. There are no positions: a widget has a width and a place in a queue.

Sources hands every widget on a page the same computed answers, so the funnel, the
response rate and the reply times cost one walk of the event log rather than three.

The registry is a plain dict filled from AppConfig.ready — widgets are not plugins
today, and nothing in Server settings → Plugins lists them.

What this asks for

  • a 4- or 5-column grid with widgets placed in it, rather than a flow of widths;
  • arrangement by dragging;
  • built-in widgets for now, spoken of as an internal widgets plugin;
  • third-party widgets in a later release.

Worth being careful about

Four or five is not a free choice, and five is the harder one. Twelve was picked
because a quarter, a half and a whole all divide into it. Four divides by halves and
quarters; five divides by nothing useful — a "half-width" widget in five columns is two
columns or three, never half, and the seventeen widgets currently declare their widths in
exactly those words. Either the widths stop being quarter/half/full and become spans
in the new grid's own units, or the grid is four. This should be settled before anything is
built on top of it.

Tailwind compiles the classes it can see. The template writes lg:col-span-3 and its
siblings out literally, with a comment saying why: a class assembled at runtime from stored
data is a class Tailwind never compiled, and it silently does nothing. A grid whose spans
and starts come out of a person's saved arrangement needs either an exhaustive written-out
set or a safelist, decided deliberately.

A grid of four or five columns has to survive a phone. #73 is open on exactly that, and
this must not assume it is solved. The stored arrangement is the wide-screen one; what a
narrow screen does with it — collapse to one column in reading order, or keep two — is part
of this issue, and the answer determines whether a person's careful placement means
anything on the device they are most likely to check on.

Right to left. Column one is the start edge, not the left. The template lint already
fails a physical direction class, and grid placement is where that is easiest to get wrong.

Holes and overlaps. A true coordinate model admits both: two widgets in one cell, and a
gap in the middle of the page. A packed model with spans admits neither and is far simpler
to store, validate and render. Which one this is decides how much validation the save path
needs, and how a grid degrades when a widget is uninstalled and leaves a hole.

"Internal widgets plugin" is a bigger phrase than it looks. The plugin kinds today
either read something outside (source, importer), talk to a service (notifier,
store, sync), carry mail (transport) or govern a capability of Postulo itself
(feature, added in #90). A widget is none of those: it contributes a thing to a page.
Making it a kind means deciding:

  • whether seventeen new rows appear in Server settings → Plugins for an administrator to
    switch off, and whether that is an improvement or a wall;
  • what switching one off does to somebody who has it in their stored arrangement — #90's
    rule is that switching a plugin off deletes nothing, so the arrangement must keep the
    key and the page must simply not render it, which is what keys_for() already does for a
    key it does not recognise;
  • whether "internal plugin" here means a real entry-point kind now, or the existing registry
    named as one and given an entry point when third parties actually arrive.

The last is the cheap answer and probably the right one for this release, given the
suggestion puts extension in a later one.

The shared Sources pass is worth keeping. Whatever the grid does, it must not end up
building each widget in isolation; several of them exist only because the expensive answer
is computed once.

Every new string is thirty-nine translations. An arrange page rebuilt around a grid will
add labels, instructions and announcements. That is the standing cost of any interface work
here and it belongs in the estimate rather than in a surprise at the end.

## Observation > the dashboard, even if inicilized for a new user, must be unique for each user. > the disposition can be flexible in a 4 or 5 x something grid, widgets organized by > drag-drop, for now using builting widget (internal widgets plugin) but later can be > extended (future release) Third of three. Blocked by the two above: one decides where an arrangement is stored, the other decides how a widget is placed by somebody not using a mouse. ## What exists Seventeen widgets are registered, seven of them in the default arrangement: | Group | Widgets | | --- | --- | | What needs doing | `suggestions`, `counters`, `gone_quiet`, `upcoming_interviews`, `due_reminders`, `recent_activity`, `shortcuts`, `listings` | | What the record says | `insight_counters`, `funnel`, `outcomes`, `durations`, `interviews_summary`, `sources`, `industries`, `by_month`, `selectivity` | Each declares a width of `quarter`, `half` or `full`, and `core/dashboard.html` turns that into `lg:col-span-3`, `-6` or `-12` **over a twelve-column grid**, in one flow, ordered by the stored list. There are no positions: a widget has a width and a place in a queue. `Sources` hands every widget on a page the same computed answers, so the funnel, the response rate and the reply times cost one walk of the event log rather than three. The registry is a plain `dict` filled from `AppConfig.ready` — widgets are **not** plugins today, and nothing in *Server settings → Plugins* lists them. ## What this asks for - a **4- or 5-column grid** with widgets placed in it, rather than a flow of widths; - arrangement **by dragging**; - built-in widgets for now, spoken of as an **internal widgets plugin**; - third-party widgets in a later release. ## Worth being careful about **Four or five is not a free choice, and five is the harder one.** Twelve was picked because a quarter, a half and a whole all divide into it. Four divides by halves and quarters; five divides by nothing useful — a "half-width" widget in five columns is two columns or three, never half, and the seventeen widgets currently declare their widths in exactly those words. Either the widths stop being `quarter`/`half`/`full` and become spans in the new grid's own units, or the grid is four. This should be settled before anything is built on top of it. **Tailwind compiles the classes it can see.** The template writes `lg:col-span-3` and its siblings out literally, with a comment saying why: a class assembled at runtime from stored data is a class Tailwind never compiled, and it silently does nothing. A grid whose spans and starts come out of a person's saved arrangement needs either an exhaustive written-out set or a safelist, decided deliberately. **A grid of four or five columns has to survive a phone.** #73 is open on exactly that, and this must not assume it is solved. The stored arrangement is the wide-screen one; what a narrow screen does with it — collapse to one column in reading order, or keep two — is part of this issue, and the answer determines whether a person's careful placement means anything on the device they are most likely to check on. **Right to left.** Column one is the start edge, not the left. The template lint already fails a physical direction class, and grid placement is where that is easiest to get wrong. **Holes and overlaps.** A true coordinate model admits both: two widgets in one cell, and a gap in the middle of the page. A packed model with spans admits neither and is far simpler to store, validate and render. Which one this is decides how much validation the save path needs, and how a grid degrades when a widget is uninstalled and leaves a hole. **"Internal widgets plugin" is a bigger phrase than it looks.** The plugin kinds today either read something outside (`source`, `importer`), talk to a service (`notifier`, `store`, `sync`), carry mail (`transport`) or govern a capability of Postulo itself (`feature`, added in #90). A widget is none of those: it *contributes* a thing to a page. Making it a kind means deciding: - whether seventeen new rows appear in *Server settings → Plugins* for an administrator to switch off, and whether that is an improvement or a wall; - what switching one off does to somebody who has it in their stored arrangement — #90's rule is that switching a plugin off **deletes nothing**, so the arrangement must keep the key and the page must simply not render it, which is what `keys_for()` already does for a key it does not recognise; - whether "internal plugin" here means a real entry-point kind now, or the existing registry named as one and given an entry point when third parties actually arrive. The last is the cheap answer and probably the right one for this release, given the suggestion puts extension in a later one. **The shared `Sources` pass is worth keeping.** Whatever the grid does, it must not end up building each widget in isolation; several of them exist only because the expensive answer is computed once. **Every new string is thirty-nine translations.** An arrange page rebuilt around a grid will add labels, instructions and announcements. That is the standing cost of any interface work here and it belongs in the estimate rather than in a surprise at the end.
tiagoagueda added this to the 0.3.0 milestone 2026-09-09 10:10:46 +00:00
Author
Owner

The premise under “internal widgets plugin” has changed. This issue offered a cheap answer — name the existing registry as a plugin and give it an entry point when third parties arrive — and #129 makes that answer unavailable: a plugin Postulo ships must carry its own manifest, its own catalogues and no reach into core.

So the widget kind now inherits the same three questions: what a widget may import, where its strings live, and (if a widget ever stores anything) what happens to that when it goes. Linked as a dependency rather than left to contradict this issue quietly.

The premise under *“internal widgets plugin”* has changed. This issue offered a cheap answer — name the existing registry as a plugin and give it an entry point when third parties arrive — and #129 makes that answer unavailable: a plugin Postulo ships must carry its own manifest, its own catalogues and no reach into core. So the widget kind now inherits the same three questions: what a widget may import, where its strings live, and (if a widget ever stores anything) what happens to that when it goes. Linked as a dependency rather than left to contradict this issue quietly.
Author
Owner

Done in 078be627, and with it the three-issue set: #123 settled where an
arrangement is stored, #124 how a widget is moved without a mouse, and this is the grid and
the gesture.

The issue said three things should be decided before anything was built on them. Here are
the three answers, since they are the whole of the design.

Four columns, not five

Four. The three widths every widget already declares — a quarter, a half, a whole row —
mean what they say in four and mean nothing in five. A half-width widget in five columns is
two columns or three and never half, so five would have meant re-measuring every widget
against a grid that divides by nothing. Four took a quarter, a half and a whole with no
arithmetic and no renaming, which is a test in itself (test_the_widths_did_not_have_to_be_renamed).

Packed, not placed

A widget has a width and a place in the order; the row it lands on falls out of the two.

That is worth stating for what it makes impossible rather than for what it does. A
coordinate model — row and column per widget — can represent two widgets in one cell and a
hole in the middle of the page, so it needs validating, repairing, and an answer for what
happens when a plugin is uninstalled and its widget vanishes from row 2. This model cannot
represent either state, so none of that code exists and none of it can rot. Uninstall a
plugin and the rest close up.

A phone gets one column, in reading order

#73 is open on the phone and this does not assume it solved. What a narrow screen gets is
the same arrangement read downwards — the only reading that needs no second answer, and
there is deliberately no sm: breakpoint offering a different one for a middle size.

Dragging, on top of the arrows

The gesture asked for, added the way every drag in this application is added: on top of a
control that works everywhere, never in place of one.
Drag and drop fires on neither a
touch screen nor a keyboard.

  • A drop posts to the same address the arrows post to, with the position it landed at.
    No new endpoint, no second store — a page arranged by dragging and a page arranged by
    pressing arrows are the same page, saved the same way.
  • It gets the same sentence back: Interviews coming up is now in row 2, place 1.
    Because it is the same move.
  • Nothing is draggable until the script makes it so. draggable is set in app.js, not
    in the template, so with scripts blocked nothing looks draggable. An affordance that does
    nothing is worse than none.
  • A drop past the last row is clamped to last rather than refused: that is somebody
    meaning last, not somebody making a mistake.

Two things deliberately not done

Widths are not the person's to choose. The issue does not ask for it, and a width is the
widget's own statement about how much room it needs to be legible — a six-stage funnel is
unreadable in a quarter of a row. What is being arranged is the order.

"Internal widgets plugin" is the registry that already exists, named as one — not a plugin
kind.
Making a widget a plugin kind would put seventeen rows in Server settings →
Plugins
for an administrator to switch off, which is a wall rather than an improvement, and
would need an entry-point contract before anybody outside is asking for one. The half of
that contract that actually matters — a key that cannot collide with another provider's,
plus Widget.provider — shipped with #123, so a plugin's widget is already namespaced and
already refused if it is not. There is a test recording that reasoning
(test_widgets_are_a_registry_named_as_a_plugin_rather_than_a_plugin_kind), so the decision
is in the repository rather than only here.

One thing the tests caught

test_the_spans_are_written_out_rather_than_assembled failed on the first run: col-span-4
was not in the compiled stylesheet. The committed CSS was stale after the grid change —
which is exactly the silent failure the issue warned about, since Tailwind only compiles
classes it can see and a class it never sees does nothing without saying so. npm run build:css fixed it, and the test exists so that it fails loudly next time instead.

Also

tests/test_dashboard_grid.py (18) and tests/e2e/test_widget_drag.py (7). Suite 4482
passed, 29 skipped; browser suite 85 passed. No new strings — the drop reuses the
sentence the arrows already had — so all 39 European catalogues were already complete.
Wiki: Insights → The grid it lands on.

Shipped on 0.3.0, with main kept level.

Done in `078be627`, and with it the three-issue set: #123 settled where an arrangement is stored, #124 how a widget is moved without a mouse, and this is the grid and the gesture. The issue said three things should be decided before anything was built on them. Here are the three answers, since they are the whole of the design. ## Four columns, not five **Four.** The three widths every widget already declares — a quarter, a half, a whole row — mean what they say in four and mean nothing in five. A half-width widget in five columns is two columns or three and never half, so five would have meant re-measuring every widget against a grid that divides by nothing. Four took a quarter, a half and a whole with no arithmetic and no renaming, which is a test in itself (`test_the_widths_did_not_have_to_be_renamed`). ## Packed, not placed **A widget has a width and a place in the order; the row it lands on falls out of the two.** That is worth stating for what it makes *impossible* rather than for what it does. A coordinate model — row and column per widget — can represent two widgets in one cell and a hole in the middle of the page, so it needs validating, repairing, and an answer for what happens when a plugin is uninstalled and its widget vanishes from row 2. This model cannot represent either state, so none of that code exists and none of it can rot. Uninstall a plugin and the rest close up. ## A phone gets one column, in reading order #73 is open on the phone and this does not assume it solved. What a narrow screen gets is the same arrangement read downwards — the only reading that needs no second answer, and there is deliberately no `sm:` breakpoint offering a different one for a middle size. ## Dragging, on top of the arrows The gesture asked for, added the way every drag in this application is added: **on top of a control that works everywhere, never in place of one.** Drag and drop fires on neither a touch screen nor a keyboard. - A drop posts to **the same address the arrows post to**, with the position it landed at. No new endpoint, no second store — a page arranged by dragging and a page arranged by pressing arrows are the same page, saved the same way. - It gets **the same sentence back**: *Interviews coming up is now in row 2, place 1*. Because it is the same move. - **Nothing is draggable until the script makes it so.** `draggable` is set in `app.js`, not in the template, so with scripts blocked nothing looks draggable. An affordance that does nothing is worse than none. - A drop past the last row is clamped to *last* rather than refused: that is somebody meaning last, not somebody making a mistake. ## Two things deliberately not done **Widths are not the person's to choose.** The issue does not ask for it, and a width is the widget's own statement about how much room it needs to be legible — a six-stage funnel is unreadable in a quarter of a row. What is being arranged is the order. **"Internal widgets plugin" is the registry that already exists, named as one — not a plugin kind.** Making a widget a plugin kind would put seventeen rows in *Server settings → Plugins* for an administrator to switch off, which is a wall rather than an improvement, and would need an entry-point contract before anybody outside is asking for one. The half of that contract that actually matters — a key that cannot collide with another provider's, plus `Widget.provider` — shipped with #123, so a plugin's widget is already namespaced and already refused if it is not. There is a test recording that reasoning (`test_widgets_are_a_registry_named_as_a_plugin_rather_than_a_plugin_kind`), so the decision is in the repository rather than only here. ## One thing the tests caught `test_the_spans_are_written_out_rather_than_assembled` failed on the first run: `col-span-4` was not in the compiled stylesheet. The committed CSS was stale after the grid change — which is exactly the silent failure the issue warned about, since Tailwind only compiles classes it can see and a class it never sees does nothing without saying so. `npm run build:css` fixed it, and the test exists so that it fails loudly next time instead. ## Also `tests/test_dashboard_grid.py` (18) and `tests/e2e/test_widget_drag.py` (7). Suite 4482 passed, 29 skipped; browser suite 85 passed. **No new strings** — the drop reuses the sentence the arrows already had — so all 39 European catalogues were already complete. Wiki: *Insights → The grid it lands on*. Shipped on `0.3.0`, with `main` kept level.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
Postulo/postulo#125
No description provided.