Skip to content

replace web ui with react frontend - #455

Open
mik3y wants to merge 48 commits into
mainfrom
mikey/new-frontend
Open

replace web ui with react frontend#455
mik3y wants to merge 48 commits into
mainfrom
mikey/new-frontend

Conversation

@mik3y

@mik3y mik3y commented Aug 4, 2026

Copy link
Copy Markdown
Member

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) alongside kegbot runserver.

The overall design and IA of the legacy dashboard remains mostly unchanged. Some modest improvements were made:

  • dropped bootstrap 3 for material ui
  • light/dark mode support
  • better responsive layouts
  • stats and session views have improved breadcrumbs + calendar views

mik3y added 30 commits August 3, 2026 15:38
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.
mik3y added 18 commits August 4, 2026 05:10
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
Sessions / 2026 / August / 3 / Session #9 / Drink #42 — the session
crumb links to the session; dates come from the session's start_time
(fetched; the drink's own date can cross midnight) in the site
timezone. Sessionless drinks keep the plain eyebrow.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant