Skip to content

feat: admin frontend for the OpenTaberna store - #2

Merged
PhilippTheServer merged 1 commit into
mainfrom
pl/admin-frontend
Aug 19, 2026
Merged

PhilippTheServer merged 1 commit into
mainfrom
pl/admin-frontend

Conversation

@PhilippTheServer

Copy link
Copy Markdown
Contributor

Feature

The back-office application: catalogue, stock, orders and returns, behind a sidenav.

Closes #1.

Heads-up on the storefront

The intent was to mirror OpenTaberna/frontend. That repository currently holds one
commit with a one-line README
— there is no Angular code to clone, run or mirror. So the
structure is defined here and documented in the README, and the storefront can follow it.
Worth telling @maltonoloco before he starts, so the two do not diverge.

Structure

src/
  app/
    core/          # singletons: Keycloak session, interceptor, guard, typed API clients
    shared/ui/     # reusable presentational components. No API calls.
    layout/        # shell, sidenav, topbar
    features/      # one lazy-loaded folder per screen
  styles.css       # the global design file

Four rules keep it honest: core/ is injected and never imported into a template;
shared/ui/ never calls the API, so a component can be reused on a screen that already
holds the data; features/ reach the API only through a core/api client; and no design
literals in templates — if a token is missing, it goes in styles.css.

The global design file

src/styles.css holds Tailwind v4 @theme tokens (colour, radius, shadow, type) plus a
small set of semantic classes — .card, .field-control, .data-table.

Semantic classes are deliberately few. Something used once stays a utility in the template;
a class earns its place only when copying the utility string around would let screens
drift. Status colour is assigned by meaning rather than by feature, so danger reads
the same on an order, a stock level and a return — the palette is learned once.

Reusable components

ot-button, ot-badge, ot-card, ot-modal, ot-page-header, ot-empty-state,
ot-spinner, ot-alert, and a money pipe. Standalone and imported per feature, so a
lazy chunk pulls in only what it renders.

ot-empty-state exists because "no data" and "still loading" otherwise look identical —
the most common way an admin screen confuses the person using it. money exists because
the API stores minor units, and dividing by 100 in each template eventually gets forgotten.

Auth

Authorization-code + PKCE via opentaberna-admin-ui, completed by an APP_INITIALIZER so
the guard never evaluates an empty session and bounces a legitimate admin. The interceptor
attaches the token only to requests aimed at the configured API host, never to a third
party.

adminGuard is a usability guard, not a security boundary — the API enforces access
itself. It exists so a signed-in customer sees an explanation instead of a screen of
failing requests.

Two bugs found by driving the running app

  1. Dashboard requested 200 catalogue items; the endpoint caps at 100. Result was a 422
    and a blank panel. The service now clamps, so no caller can trip it again — with a test.
  2. The product form offered an inactive status the API does not have. Its enum is
    draft | active | archived, so saving anything but active failed. Corrected, and the
    copy now says which states are hidden from customers.

Neither was visible from a build. Both came out of clicking through the real thing.

Verification

Driven headlessly against the live stack (API, Keycloak, Postgres, Redis, MinIO):

after load -> http://localhost:8080/realms/opentaberna/protocol/openid-connect/auth
after login -> http://localhost:4200/dashboard
  /dashboard -> h1: "Good to see you, Admin User"
  /products  -> h1: "Products"
  /inventory -> h1: "Inventory"
  /orders    -> h1: "Orders"
  /returns   -> h1: "Returns"
console errors: none
failed API calls: none

Also confirmed:

  • Products created through the form, not the API — the whole write path including
    token attachment and list refresh.
  • A duplicate SKU surfaces the API's own message (Item with sku='TAB-RED-001' already exists) rather than failing silently.
  • A customer account (testuser) lands on /forbidden with "This account is not an
    administrator".
  • The sidenav collapses to a drawer at 480px wide.
$ npx ng test --watch=false
Test Files  4 passed (4)
     Tests  16 passed (16)

$ npx prettier --check "src/**/*.{ts,html,css}"
All matched files use Prettier code style!

$ npx ng build --configuration production
Application bundle generation complete.

CI runs all three on every PR.

Not included

  • Returns has no list endpoint in the API yet, so that screen works by return id. Listing
    should follow once the API exposes it.
  • Order status override is available; creating shipments and triggering DHL labels are not
    yet surfaced.
  • The production environment.prod.ts carries placeholder hosts, to be set at deploy time.

Angular 22 back-office for administering what customers see: catalogue,
stock, orders and returns. Sidenav shell, Tailwind v4, one global design
file, reusable components.

Structure

  core/     singletons - Keycloak session, HTTP interceptor, admin guard,
            one typed client per API domain, models mirroring the schemas
  shared/   presentational components that never call the API, so they can
            be reused on a screen that already holds the data
  layout/   the frame: shell, sidenav, topbar
  features/ one lazy-loaded folder per screen

src/styles.css is the single source of truth for appearance: Tailwind
@theme tokens plus a small set of semantic classes for patterns repeated
across screens. Status colour is assigned by meaning rather than by
feature, so danger reads the same on an order, a stock level and a
return.

Auth

Authorization-code + PKCE through the opentaberna-admin-ui client, run by
an APP_INITIALIZER so the guard never evaluates an empty session. The
interceptor attaches the token only to requests aimed at the configured
API host, never to a third party. The guard is for usability, not
security - the API enforces access itself, and the guard exists so a
signed-in customer gets an explanation instead of failing requests.

Two bugs found by driving the running app

The dashboard requested 200 catalogue items where the endpoint caps at
100, producing a 422 and a blank panel. The service now clamps, so no
caller can trip it again.

The product form offered an "inactive" status the API does not have - its
enum is draft, active, archived - so saving anything but active failed.
Corrected, and the copy now says which states are hidden from customers.

Verified against the live stack: the Keycloak round trip completes, all
five routes render with real data, no console errors, no failed API
calls, the sidenav collapses to a drawer below lg, a duplicate SKU shows
the API's message rather than failing silently, and a customer account
lands on the "not an administrator" screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@PhilippTheServer
PhilippTheServer merged commit a9dd74f into main Aug 19, 2026
1 check passed
@PhilippTheServer
PhilippTheServer deleted the pl/admin-frontend branch August 19, 2026 18:45
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.

Build the admin frontend: sidenav shell, Tailwind, reusable components

1 participant