replace web ui with react frontend - #455
Open
mik3y wants to merge 48 commits into
Open
Conversation
KEGBOT_ADMIN_LOGIN_URL and the activation-complete template referenced
the url name "auth_login", which no longer exists (the route is named
"login"), breaking staff_member_required redirects with NoReverseMatch.
Also remove pykeg/backend and pykeg/contrib/{twitter,untappd,foursquare},
which contained only stale __pycache__ left over from earlier removals.
Adds django-filter with FilterSets for drinks (user/keg/session/username), kegs (status), sessions (year/month/day, powering the date archives), events (since, for pollers), thermo logs (sensor/time range), and users + auth tokens (search, replacing the kegadmin autocomplete views). Cursor pagination now honors ?page_size (capped at 100). Ordering stays fixed at -id since cursor pagination owns the sort order. Adds GET /api/sessions/current, returning the active session or 404.
GET /api/users/me is the frontend boot endpoint. It always responds 200 regardless of authentication or site privacy: user is null for anonymous callers, and the payload is limited to privacy-safe site configuration (plus can_invite, sso urls, and the plugin list) that the frontend needs to render login screens and interstitials. It also sets the CSRF cookie so a fresh browser session can immediately make authenticated POSTs. It supersedes /api/auth/current-user, which is removed. Static constants (choice lists, keg sizes, timezones) are deliberately not served over the API: the new print_constants management command writes them to web-ui/lib/shared-constants.ts, which gets baked into the frontend build. When setup or upgrade is required, /api/* requests now receive a JSON 403 instead of a rendered HTML page, and /api/setup/ is exempted for the upcoming setup-wizard API. Drops django-cors-headers and the hardcoded localhost:1234 CORS/CSRF origins: frontend development runs through the vite dev-server proxy, so browser requests are always same-origin.
Adds stats accessor endpoints backed by the existing model helpers:
GET /api/stats/system, /api/users/{username}/stats, /api/kegs/{id}/stats,
and /api/sessions/{id}/stats. Blobs come from Stats.get_latest_for_view,
which already maps drinker ids to usernames.
UserViewSet now uses username as the lookup key, matching the public
drinker-page URLs the frontend preserves; the lookup regex admits dotted
usernames. User detail and stats are viewable per site privacy (like the
old public drinker pages) while the full user listing still requires
authentication.
KegTapViewSet becomes a full ModelViewSet (admin writes, privacy-gated reads) with actions delegating to the existing domain methods: attach-keg, start-keg, end-keg, connect-meter, connect-toggle, connect-thermo, and record-drink (which also handles spills, mirroring the kegadmin tap page). KegViewSet gains create (Keg.create_keg, with optional on-the-fly beverage creation), notes/description editing, end, reactivate, spill, and delete (Keg.cancel, destroying the keg and its drinks). Status and volume fields are read-only on direct edits: they change only through the lifecycle endpoints.
Drinks gain owner/admin mutations mirroring the old drink and kegadmin
pages: owners may edit the shout and attach or erase the drink picture;
admins may adjust volume (Drink.set_volume), reassign to another user,
and delete (cancel_drink, with ?spilled=true to move the volume to
spillage). Creation still only happens via pours.
Adds the first picture upload endpoints: POST/DELETE /api/drinks/{id}/picture
plus POST {id}/picture on beverages and producers (admin), replacing the
kegadmin image-upload forms. Beverage and producer serializers now embed
the picture object instead of a bare id.
New IsOwnerOrAdmin object-level permission.
Account endpoints (authenticated): PATCH /api/users/me (profile),
POST /api/account/{password,email,confirm-email,mugshot,regenerate-api-key}.
The email change flow reuses the existing signed-token helpers and
confirmation mail template.
Auth flows (unauthenticated, rate-limited): POST /api/auth/register
(honors registration_mode and consumes invitations), password-reset +
password-reset-confirm (reusing the Django reset-token machinery and
mail template; no account enumeration), and /api/account/activate for
invited accounts (single-use activation keys).
InvitationViewSet gains create (sends the invitation mail, gated on
kbsite.can_invite) and delete.
UserViewSet gains admin operations mirroring the kegadmin users pages: create (optionally staff), edit (email, display name, enable/disable, staff status), and set-password. The guest account cannot be disabled or promoted; there is still no user delete, matching the old UI. Adds GET/PATCH /api/site (admin-only singleton) covering the fields of the old general/location/advanced settings forms, plus a background image upload endpoint. Auth tokens can now be assigned to a user on create/update.
Adds /api/admin/{dashboard,backups,logs,email-test,bugreport} (all
admin-only), replacing the corresponding kegadmin pages: dashboard
health summary (email config, redis ping, user counts), backup listing
with archive metadata plus build (async via the existing RQ task) and
delete, the redis log viewer (defensive when redis is down), the test
email sender, and an in-process bugreport (no more shell=True).
Adds /api/setup/{status,migrate,settings,admin-user,finish,upgrade},
powering the frontend setup flow. The endpoints are open only while
setup or upgrade is required (SetupAccess permission), use no
authenticators since the database may not exist yet, and are exempt
from the IsSetupMiddleware gate.
Unlike the old cookie-driven wizard (which was also DEBUG-only), there
is no server-side inter-step state: the frontend submits collected
choices in a single settings call. The admin-user step logs the new
superuser in, and finish/upgrade stamp the current server version.
GET /api/admin/plugins lists installed plugins; GET/PUT
/api/admin/plugins/{short_name}/settings reads and updates a plugin's
site settings, validating writes through the plugin's own Django form
so field errors surface in standard DRF shape. Replaces the
template-based plugin admin settings views (webhook is the only
in-tree plugin).
Adds ENUM_NAME_OVERRIDES so keg status and event kind get descriptive component names in the schema, and commits the generated spec at web-ui/api/schema.yaml — the contract the frontend's generated client builds against (regenerated via kegbot spectacular).
Adds the bun + vite + react + MUI + TypeScript + Biome toolchain, with
configs at the repo root and all frontend source under web-ui/:
- vite dev server proxies /api, /media, and /static to Django, so
development is same-origin with hot reload (bun run dev + kegbot
runserver, two processes)
- generated, committed API client (bun run generate-api: drf-spectacular
schema at web-ui/schema.yaml -> @hey-api/openapi-ts into web-ui/api/,
excluded from linting)
- lib/api.ts configures the client (same-origin cookies + X-CSRFToken
from the csrftoken cookie) and normalizes DRF field errors
- lib/use-async-data.ts: {data, loading, error, reload} hook with
optional polling and deps-driven refetch, plus bun test + happy-dom +
testing-library coverage
- app shell: stock MUI theme, splat data-router wrapping a classic
<Routes> tree, main/minimal layouts, placeholder home view
- imports are root-relative via the @/ alias; parent-relative imports
are a Biome error
The app now boots from GET /api/users/me: ConfigProvider blocks render until the payload arrives (spinner / retry / setup-required branches), then provides site config; CurrentUserProvider layers login/logout and user state on top of the same payload. SnackbarProvider gives toast notifications and ConfirmProvider promise-based confirmation dialogs. PrivacyGate enforces the site privacy setting around the main layout, rendering members-only/staff-only interstitials with a login link. The main layout shows the site title and a user menu (account, admin for staff, logout), honoring SSO login/logout URLs when configured. Auth views (minimal chrome, old URL shapes preserved): login, register (public/invite modes via ?invite_code), password reset request/confirm, account activation, e-mail change confirmation, logout. Forms map DRF field errors onto inputs; shared Page/LoadingZone components carry titles and async states. Tests cover boot, privacy gating, setup detection, login error mapping, and the reset-link parser, over a mocked fetch layer.
Adds the read-side of the site: - home: on-tap cards (keg fill, beverage, live tap temperature from thermo logs) beside the recent-activity event timeline, polling /api/status every 10s - fullscreen kiosk mode: same data, chrome-free, re-renders instead of reloading like the old page - stats: headline badges plus top-drinkers and volume-by-weekday charts (MUI X Charts) from /api/stats/system - kegs: list with status chips; detail with fill/spill info, per-keg top drinkers, sessions (derived from the stats blob), and its drinks - drinkers: profile with stats badges, weekday chart, sessions, drinks - drinks: pour detail with shout, session link, picture display and owner/admin picture erase - sessions: archive honoring the old /sessions/[year[/month[/day]]] URL hierarchy via API date filters, plus detail with per-drinker chart; /sessions/id/:id resolves sessions by id (old /s/:id short links redirect there) Shared pieces: unit-aware formatters bound to site settings, cursor 'load more' pagination hook, event timeline, tap/keg cards, drink tables, and chart components. Main nav gains Kegs/Sessions/Stats.
Adds the authenticated /account section (tab chrome, login-redirect guard): account overview with the staff API-key panel and regenerate flow, profile (display name + mugshot upload), password change, notifications (e-mail preference checkboxes over notification-settings plus the change-e-mail confirmation flow), and invitations (send and revoke, shown only when the site's registration mode allows inviting). The OpenAPI schema now emits split request components (COMPONENT_SPLIT_REQUEST), so generated request types drop read-only fields and type binary uploads as files; schema and client regenerated.
Adds the staff-only /kegadmin section as a lazy-loaded chunk with a sectioned side nav (entries honor enable_sensing/enable_users): - dashboard: email/redis health warnings and user counts - settings: general (title, privacy, registration, feature toggles), location (units + timezone autocomplete), advanced (session timeout, analytics, email config URI, background image upload), and the e-mail status/test page - taps: list/create, plus the per-tap operations hub — attach an available keg or start a brand-new one, end keg, record manual drinks/spills, connect meter/toggle/thermo sensor, rename, delete - keg room: status-filtered keg list with per-keg actions (finish, reactivate, spill, delete-with-drinks), description editing, and add-keg form using shared keg-size constants - beverages and producers: dialog-based CRUD plus picture uploads - controllers: controller CRUD with nested flow meter/toggle management - users: search, create (optionally staff), enable/disable, grant/revoke staff, set password; drinks: reassign/cancel/cancel-as-spill; tokens: CRUD with user assignment and search - maintenance: logs viewer, bugreport generator, backup/export (build, poll, download, delete), and generic plugin settings (webhook)
When the boot request reports setup_required or upgrade_required, the app renders a full-page flow instead of the site: a five-step wizard (migrate the database with output shown, hardware sensing toggle, user-accounts toggle, initial site settings, admin account) that submits collected choices in one settings call before creating the admin and finishing — no server-side inter-step state — or a one-click upgrade page showing installed vs. new version. Both reload into the freshly-configured app on success.
Production: the vite bundle (built with base=/static/) is collected
into the static tree; a catch-all route (everything except api, media,
static, and admin paths) renders the SPA shell from vite's manifest via
{% static %}, composing with WhiteNoise's hashed-manifest storage, and
sets the CSRF cookie. The Dockerfile gains a bun build stage whose
output lands before collectstatic. Both UIs coexist for now: existing
server-rendered URLs still win; the SPA serves everything else.
Development: browsing http://localhost:8000 now just works — vite owns
8000 and proxies /api, /media, and /static to Django, whose runserver
now defaults to 8001 (pykeg.core precedes whitenoise in INSTALLED_APPS
so its runserver override wins).
The generated client moves from web-ui/api/ to web-ui/api-client/: with
vite's root serving sources at /<dir>, the old location's module URLs
collided with the /api proxy prefix and came back as HTML.
The old kegweb short-link test now uses the canonical slashed URLs,
since slash-less paths fall through to the SPA instead of APPEND_SLASH.
The React frontend is now the only web UI. Deleted: the kegweb, kegadmin, account, kbregistration, and setup_wizard apps (views, urls, forms, templates), the server-side chart builders, the vendored jQuery/Bootstrap/Highcharts static tree, the context processor, the staff_member_required decorator, and the crispy-forms / django-registration dependencies. What remains and moved: - e-mail templates (notification mails, registration/activation/invite mails, password reset) stay under pykeg/web/templates; their links resolve through named SPA stub routes that preserve the legacy URL names (kb-drink, registration_register, password_reset_confirm, ...) for get_absolute_url and mail rendering - PasswordResetForm relocated to pykeg.api.forms; the controller/meter forms the legacy api used relocated to pykeg.web.api.forms; the media storage backend moved to pykeg.web.kbstorage - the webhook plugin keeps its logic and settings form (managed via the plugin settings api); the plugin framework drops its template-view url hooks PrivacyMiddleware is gone: privacy is enforced solely by API permissions, with the frontend rendering the interstitials. IsSetupMiddleware now only gates /api/* (JSON 403); all other paths fall through to the SPA shell, which boots into the setup wizard, and its pre-migration session stub now covers /api/setup.
Documents the React UI replacement and the API additions in the 2.0.0 changelog (including the bun build requirement and the new two-process dev flow), and adds a Development section to the README.
Vite proxy keys are prefix matches, so "/api" also captured source modules under /api-client/ and returned them as HTML from Django. Regex keys anchored at a path-segment boundary proxy only /api, /media, and /static themselves.
Replaces the stock MUI look with a deliberate system: cool neutral surfaces with a single amber accent, light and dark color schemes (following the OS by default, switchable from the nav, persisted), and IBM Plex type — Plex Sans for UI, Plex Mono for data: numerals, table headers, eyebrow section labels, and the wordmark. Fonts are bundled locally via fontsource. Chrome: hairline paper AppBar with the mono KEGBOT wordmark and amber tap glyph, quiet nav with an active state, and the color-mode toggle. The account area adopts the same side-nav layout as admin (shared SideNavLayout with an amber active edge); auth pages get the wordmark. Data display: chart series colors validated for contrast and CVD separation on both surfaces (amber/teal/green/plum), thin rounded bars and recessive axes; stat badges become mono-numeral tiles; the keg traffic-light progress bar becomes a ticked gauge with a mono readout (low levels recolor the readout, not the fill). Polish: window.prompt admin flows replaced with a promise-based input dialog, dashed-border empty states with hints, accent-colored letter avatars, buttons lose the all-caps.
The tap deck is now the hero: a full-width responsive row of redesigned
tap cards, with the activity feed below in a constrained measure —
replacing the inherited two-column layout that gave the log feed the
wide column and the taps the leftovers.
Tap cards get a fixed internal grid so a row of taps lines up: eyebrow
tap name with a mono temperature readout, the beverage name at display
size, one quiet metadata line (producer · style · ABV), and the gauge
band bottom-aligned.
The activity feed drops its two-line stacks for a one-line grammar —
avatar/keg-glyph gutter, sentence ('alice poured 12.0 oz — "cheers!"'),
compact mono time ('4m') in the right gutter.
Page grows one standard header anatomy (mono eyebrow / avatar + title / metadata line / actions) and a declared content-width intent (wide, content, narrow), replacing three competing header patterns: keg, session, and drink details use eyebrow+meta headers at reading width; the drinker page's hand-rolled hero and the account page's 'Hello' block become Page headers. Charts and gauges leave their Card boxes: content now sits directly under Section eyebrows (stats, keg, drinker, session views), ending the border-in-border-in-border nesting. Stat badges become a single hairline-divided strip instead of four floating cards. The drink page gets a proper quote treatment for shouts.
One set of rules for every table: the row's primary column carries the accent link (drinks link from a compact mono relative time, absolute on hover); entity links (drinkers, kegs) are quiet inherit-colored links; numerals and times are mono and right-aligned where numeric; secondary text columns (shouts, dates) drop to the secondary ink.
Fullscreen mode gets its own treatment instead of the homepage grid with a big title: always dark (scoped color scheme, regardless of the viewer's preference), wordmark header with a live mono clock, oversized tap cards (larger names, more padding) taking two thirds of the screen, and a trimmed activity feed beside them.
- The keg gauge is chunkier (22px track) with a body-size mono readout, matching its importance on tap cards and the keg page. - The drinker page stops reporting '1 drinker': its stat strip now shows total poured / pours / sessions / biggest pour (linked to the record-setting drink). StatStrip accepts arbitrary cells so pages compose stats that make sense for them. - The drink page earns its space: shout quote, a facts strip (volume, pour time, session, keg), the photo under its own section, and a 'More from this session' list linking back to the session. - The stats page's largest-session line becomes an amber-edged record callout with the volume at display size.
The /sessions/[year[/month[/day]]] routes survived the rewrite but
nothing linked into them. Now the archive exposes the hierarchy:
- a mono breadcrumb trail (Sessions / 2026 / August / 3) on every
archive level and on session detail pages (ending in the session,
with each ancestor clickable), via a new Breadcrumbs component that
Page renders in place of the eyebrow
- drill chips under the header (years at the root, months within a
year, days within a month), derived from the loaded sessions
- archive page titles name the range ("August 2026") and each row's
started-at links to that day's view
'Powered by Kegbot' (linked to kegbot.org) at the bottom of the main layout: paper surface with a hairline top border, small centered muted text, pinned below the content.
Enumerates the dates that have sessions — a tree of years, months (with day lists), and counts, newest first. Buckets use the site's active timezone, the same conversion the year/month/day list filters apply, so a directory entry always matches its filtered listing. This lets the frontend archive show complete drilldown navigation instead of deriving dates from whatever page of sessions happens to be loaded. Schema and generated client updated.
Drill chips now come from GET /api/sessions/directory — the complete, server-bucketed archive tree — instead of whatever page of sessions happened to be loaded, so every year/month/day with sessions is navigable from the start. Row day-links and session-detail breadcrumbs derive their dates in the site's timezone (Intl, with a browser-local fallback), matching the API's bucketing so a link never lands on the wrong day.
- new MonthCalendar: weekday-aligned day grid (weeks start Sunday), session days highlighted/linked, today ring, mono styling - year page shows all 12 months as mini calendars, month names link - month page shows one full calendar - monthName moved to lib/format
- mono year + session count in clickable outlined tiles, amber border on hover; responsive 2-4 across
- day view renders a large card per session: linked title, time range, pour count, big mono volume, per-drinker chart - month view: sessions left, calendar in a right column (calendar stacks on top on small screens) - session table extracted to a local component
The stats blob keys weekdays by strftime("%w") ("0"=Sunday.."6"),
not day names; the chart was looking up "monday" and plotting zeros.
Sunday-first display to match the archive calendars. Server test now
pins the key shape.
- local biome-check hook running the package.json biome via bun - local typecheck hook running bun run typecheck (tsc --noEmit) on ts/tsx changes - biome.json: ignore patterns migrated to biome 2.5 form
runserver + bun run dev, frontend commands, and biome/tsc in the lint section
The old page urls now serve the spa shell, which 503s without a frontend build; the upgrade job doesn't need one.
Vite's string-shorthand proxy implies changeOrigin, so Django saw
Host: localhost:8001 with Origin: localhost:8000 and rejected
authenticated posts ("CSRF Failed: Origin checking failed").
Shared FormErrorAlert renders any error not claimed by a rendered field (csrf failures, permission errors). Adopted in every form view; replaces the inline non-field alert blocks, and covers the views that previously showed nothing.
Directory buckets and year/month/day filters converted start_time in sql, which uses CONVERT_TZ on mysql and silently yields NULL when the server's timezone tables aren't loaded — a null year tile linking to /sessions/null, and empty filtered listings. Bucket in python and filter by datetime range instead. web-ui: parse route params with intParam so malformed values (null, NaN) never reach an api query; drop non-integer directory years.
form.initial omits unconfigured fields, leaving the webhook settings page with no inputs on a fresh site.
Axis ticks showed raw mL while tooltips showed converted units; new volumeTick formatter renders terse ticks (12 oz, 3 pt, 1.5 L).
It's used inside <p> typography (event sentences); a Stack div there is invalid nesting and triggers hydration warnings.
Parse the redis log records into time / level chip / logger / message rows, interpolating %-style args; tracebacks expand inline. Raw text still shown for unparseable records.
Time-of-day only (full date on hover), truncated logger column, and a persisted Dense switch that renders a terminal-style line stream.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #452: Rewrite the web ui to be a client-side react app.
The entire dashboard is now a ReactJS app, driven by the new API. Several new APIs were introduced in support of this, covering gaps & features previously handled by django templates and views.
This adds
bun,vite, and a variety of other tools. The webapp gets built with the docker container and the bundles are copied into staticfiles and shipped with it.In dev, one now must run
bun dev(hot reloading client-side webapp + proxy to backend) alongsidekegbot runserver.The overall design and IA of the legacy dashboard remains mostly unchanged. Some modest improvements were made: