mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-10 05:53:05 +09:00
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>
62 lines
2.4 KiB
Python
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}>'
|