Skip to content

Web Frontend Component

The Web Frontend Component adds server-rendered pages to your project: Jinja2 templates, htmx for interactivity, Alpine.js for client-side state, styled with Tailwind CSS and DaisyUI.

It is additive. The Flet frontend is core and always present; selecting htmx puts a second, HTML-first surface beside it, served by the same webserver process:

                    one webserver process
   ┌────────────────────────────────────────────────┐
   │                                                │
   │   /            htmx pages (Jinja2 templates)   │
   │   /dashboard   Flet Overseer dashboard         │
   │   /overseer    server-rendered Overseer page   │
   │   /api/v1/...  FastAPI routes                  │
   │   /static/...  fingerprinted assets            │
   │   /health      component health                │
   │                                                │
   └────────────────────────────────────────────────┘

Use the Flet dashboard for operator surfaces and internal tooling; use the web frontend for anything public-facing, content-heavy, or SEO-relevant. Both call the same services, so business logic is written once.

Getting it

On a new project:

aegis init my-app --components htmx

On an existing project:

aegis add htmx

Either way you get a styled landing page at /, the Overseer dashboard untouched at /dashboard, and a web_frontend entry in /health. Removal is symmetric: aegis remove htmx deletes the component tree and regenerates the shared files that referenced it.

Overseer comes with it at /overseer: every component and service as a list, cards or a map of the stack, each linking to its own page. The map's Traffic chip tones each node by how much ran through it instead of its health: every call along the lines touching it, Overseer's own reads included, counted as it happens with no tracer needed. Opened up, each service that breaks the service standard says how many places it should fix. To sign in to it, and keep it to admins, add auth:

aegis init my-app --components htmx --services 'auth[basic]'

Then open /overseer/login. The form uses the shared auth and form macros and delegates to the existing auth endpoint.

Who can open the Overseer

The Overseer is operator access to the deployment: it shows its routes, source and live data, and runs user administration, scheduler jobs and model loads. Without the auth service it is open to read: whoever reaches the app may use it, like any route a stack serves without auth. Every write it makes (Restart, Secrets, a refund, a test send, an upload, a saved edit) is made only on the local dev build (BUILD_ID not yet stamped by aegis deploy), opened at localhost, 127.0.0.1 or [::1]; anywhere else it is refused and its controls hidden, with a line saying why. With auth, every Overseer page, live stream and action is open to admins only, not to every signed-in account. An admin is:

  • an account whose email is in ADMIN_USER_EMAILS, or
  • an account with the admin role, when the auth service runs at the RBAC level (auth[rbac]).

With AUTH_ENABLED=false (local development) the Overseer opens without signing in: a request with no session resolves to the dev user, an admin.

Set the allowlist in .env as a JSON list, then restart the server:

ADMIN_USER_EMAILS=["you@example.com"]

With auth on and an empty allowlist, nobody reaches the Overseer: a signed-in account that is not an admin gets an "Admins only" page saying how to get in, and its fragments and streams answer 403. Signed out, Overseer pages redirect to /overseer/login. The check is one dependency on the pages router (overseer_access.py, the one module that knows whether the stack has auth), and tests/web/test_overseer_admin_gate.py walks every Overseer route as a non-admin, so a new page cannot skip it; without auth, tests/web/test_overseer_open.py walks them signed out, and nothing refuses. require_admin, for API routes you protect yourself, uses the same definition (is_admin); the API's built-in operator routes keep their own checks.

Basic auth has no email verification step. New registrations are ready immediately and the registration form redirects to /overseer. Email based password reset is also absent from basic auth because this configuration has no mail delivery; signed-in password changes remain available.

The shared feedback, form, layout, and table macro files were compared with aegis-pulse and aegis-steward before adding this page. Pulse's shared feedback, form, and table macros already match the template. The generic updates brought in from those projects are the dialog behavior, stable search field ID, and empty-state marker. Steward's account, bill, matter, import, register, and chat macros depend on its own routes and data, so they remain in that application.

What ships

app/components/web_frontend/
├── main.py                  # router wiring
├── rendering.py             # Jinja2 env, render() (page or fragment), with_toast()
├── assets.py                # static() helper, CachedStaticFiles
├── filters.py               # money / short_date / pct
├── build.py                 # asset fingerprinting -> static/dist/manifest.json
├── build_watch.py           # dev watcher: re-fingerprint on change
├── routes/
│   ├── pages.py             # page handlers (the landing, auth pages)
│   └── partials/            # fragment-only routes
├── templates/
│   ├── base.html            # document skeleton every page extends
│   ├── layouts/             # page.html and fragment.html, picked by render()
│   ├── pages/               # one template per page
│   └── components/          # macros, feedback macros, landing sections
└── static/
    ├── input.css            # Tailwind entry
    ├── css/app.css          # hand-authored styles
    └── js/app.js            # toasts and htmx error hooks

package.json                 # Tailwind + DaisyUI + Biome (devDependencies only)
tailwind.config.js           # theme + the single rebrand point

Plus gated tests (tests/components/test_web_frontend.py and the web test kit under tests/web/), Makefile targets (build-static, lint-frontend, format-frontend), a Docker css-build stage, and two dev-only compose services for hot CSS rebuild.

First custom page

  1. Add a template:

    <!-- app/components/web_frontend/templates/pages/about.html -->
    {% extends "base.html" %}
    {% block title %}About - {{ project_name }}{% endblock %}
    {% block content %}
    <section class="py-10">
      <h1 class="text-2xl font-semibold text-white">About</h1>
    </section>
    {% endblock %}
    
  2. Add a route in app/components/web_frontend/routes/pages.py:

    @router.get("/about", response_class=HTMLResponse, include_in_schema=False)
    async def about(request: Request) -> HTMLResponse:
        return templates.TemplateResponse(request=request, name="pages/about.html")
    

That is the whole loop. In dev no build step is required: static() falls back to unhashed asset paths and pages render immediately. Run make build-static when you want the fingerprinted, long-cacheable variant.

The rest of these docs

  • Templates and rendering: the base layout, the htmx conventions, and two rules that are load-bearing.
  • Styling and theming: the DaisyUI theme and the one place brand colors live.
  • Live updates: snapshots, SSE, and shared health sampling.
  • Asset pipeline: fingerprinting, the manifest, cache policy, and the dev watcher loop.
  • Auth pages: what you get when the project also selects the auth service.
Was this page helpful?