Files
ServerKit/backend/app/models/dashboard.py
T
Juan DenisandClaude Opus 5 010b632530 feat(dashboard): boards that survive the browser they were built in
The dashboard's layout lived in localStorage as `{id, visible}` — a visibility
toggle wearing a layout's clothes. It carried no geometry, so widget width was
baked into SCSS (`.dash-kpi { grid-column: span 3 }`) and "make the chart wider"
was a code change. And it stayed on one machine.

`DashboardBoard` gives a user real boards: named, ordered, each holding a JSON
array of placed widgets with x/y/w/h and per-instance config. Three defaults
ship — overview, infrastructure, apps & deploys — seeded on a user's first GET
and restorable per board.

Validation is deliberate about the boring cases: geometry must fit the 12-column
grid, ids must be unique within a board, bools are rejected as coordinates
(`bool` subclasses `int`), and a rejected PUT leaves the stored board untouched
rather than half-applied. A board belonging to another user is a 404, not a 403
— there is no reason to confirm it exists.

The shipped defaults are checked by rebuilding their occupancy map cell by cell,
so editing DEFAULT_BOARDS into an overlap fails the suite instead of shipping a
broken first-run layout.

31 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 19:30:51 -04:00

62 lines
2.4 KiB
Python

"""Per-user dashboard boards (plan 62).
A *board* is one tab on the dashboard — a name, an icon, and an ordered bag of
widget instances laid out on a 12-column grid. Boards belong to a user, not to
the panel: two operators looking at the same server keep their own tabs, their
own widget placement and their own per-widget config.
The shipped defaults ('overview', 'infra', 'apps') are seeded as real rows the
first time a user reads their boards, so a user can drag, resize and delete them
like anything else. ``slug`` remembers which shipped default a row came from,
which is what makes ``POST /<id>/reset`` able to restore it; user-created boards
leave it NULL.
``widgets`` is a native JSON column (the dominant pattern in this codebase, and
dialect-agnostic across SQLite and PostgreSQL) holding the widget-instance list
``[{i, type, x, y, w, h, cfg}, …]``. The panel does not interpret ``cfg`` — that
is the widget renderer's business — but the API layer does validate the shape of
the geometry fields before storing anything.
"""
from datetime import datetime
from app import db
class DashboardBoard(db.Model):
"""One dashboard tab belonging to one user."""
__tablename__ = 'dashboard_boards'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer,
db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True,
)
# The shipped-default id this board came from ('overview' / 'infra' /
# 'apps'). NULL for boards the user created themselves — those have no
# default to reset to.
slug = db.Column(db.String(64), nullable=True)
name = db.Column(db.String(120), nullable=False)
icon = db.Column(db.String(64), nullable=True, default='grid')
position = db.Column(db.Integer, nullable=False, default=0)
widgets = db.Column(db.JSON, nullable=False, default=list)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'slug': self.slug,
'name': self.name,
'icon': self.icon or 'grid',
'position': self.position if self.position is not None else 0,
'widgets': self.widgets or [],
}
def __repr__(self):
return f'<DashboardBoard {self.id} u{self.user_id} {self.name!r}>'