v2 change summary: v1 scoped PyFinBot's web UI as a self-contained build (its own templates, its own JWT-cookie auth plumbing). v2 reframes it as the first (alongside BottleBot's retrofit) consumer of two new shared packages —
greentechhub-coreandgreentechhub-ui— so PyFinBot doesn't reinvent auth/UI plumbing that BottleBot already half-built and every future service would build again.v3 change summary: v2 was written while
greentechhub-core/greentechhub-uiwere still design docs (the companion*_Project_Plan.mdfiles it pointed to no longer exist — both packages are now real, versioned repos with their own README/TODO/docs). This pass corrects v2's §5 against the packages' actual current source: the auth adapter mechanism it described (greentechhub_core.auth.local.LocalAuthAdapter, an abstractAuthAdapter, shared CSRF middleware) doesn't exist under those names or in that package — the real, already-shipped mechanism lives ingreentechhub-fastapi(register_auth,Depends(get_current_user)) built ongreentechhub-core'sidentitymodule. The underlying architecture (pluggable adapter,localnow,forward_auth-via-Authentik later, config not code) is unchanged and confirmed sound — only names/locations and a few gaps (no CSRF middleware, no login template, two undocumented routers) are corrected.greentechhub-fastapi/TODO.mditself flagged this brief as needing exactly this follow-up.
PyFinBot currently ships as a FastAPI JSON API only (/api/..., JWT bearer auth, interactive docs at /docs). This brief scopes a browser-based front end so a user can log in, manage stocks and transactions, run imports, and view reports without hitting the API directly — built on the shared greentechhub-core/greentechhub-ui packages rather than as a one-off.
- Stack: FastAPI + Uvicorn, SQLModel/SQLAlchemy 2.0 (async), Alembic migrations, PostgreSQL (SQLite in tests).
- Auth:
OAuth2PasswordBearer—POST /api/auth/loginexchanges credentials for a JWT (HS256, 24h expiry, no refresh/revocation). Every route but registration and login requiresAuthorization: Bearer <token>. - Data model:
User(string PK) →Transaction(FKuser_id, cascade delete) →Stock(FKstock_id, no cascade).Transactioncarries computed fields (total_value,cost,fy) derived server-side on insert. - Routers:
auth,users,stocks(CRUD + market sync +search),transactions(CRUD, paginated viafastapi-pagination),transactions/import(CSV/Excel bulk import with row-level validation and dedupe),emails(POST /api/emails/sync-commsec— Gmail IMAP ingestion of Commsec buy/sell confirmation emails),dividends(POST /api/dividends/sync— yfinance dividend history sync),reports(holdings snapshot, FY capital gains, FY dividend income). - Known gaps (from the project's own backlog): no RBAC, stateless JWT with no revocation — unsolved anywhere in the ecosystem yet, not PyFinBot-specific to fix. CORS/env-mode config now exists (
ENVIRONMENT/CORS_ORIGINSincore/settings.py), built independently ofgreentechhub-fastapi's equivalentregister_core(see §10 for the resulting duplication to clean up).
- Auth login (against existing JWT endpoint)
- Create/edit forms for stocks and transactions
- Filterable, sortable, paginated tables (transactions, stocks, import results)
- Relational field mapping — transaction rows reference
stock_id/user_id; forms and tables need to resolve these to human-readable symbol/name rather than raw FKs - Popovers (inline help / row detail)
- Toast/notification messages for async actions
- Modal dialogs (create/edit forms, delete confirmation)
- Bootstrap 5 as the component/styling base
- A dashboard view
Stack: FastAPI + Jinja2 + HTMX + Alpine.js + Bootstrap 5 — supplied by greentechhub-core and greentechhub-ui, not implemented per-service.
What v1 got right and v2 keeps: server-rendered, same-origin (no new CORS surface), no Node build chain, HTMX for the CRUD-heavy interactions. What changes: PyFinBot doesn't own the base layout, the component macros, the theme, the auth-cookie plumbing, health endpoints, or pagination/filter parsing — those come from the shared packages, the same way BottleBot's retrofit and any future service consume them.
Addition over v1: Alpine.js sits alongside HTMX — HTMX handles server round-trips (table filter/sort/pagination, form submits), Alpine handles pure client-side interaction (dropdowns, tabs, the dark-mode toggle, popover open state) without hand-written JS. Note: greentechhub-ui's actual shipped dark-mode toggle (gth_theme_toggle, confirmed in its TODO.md) is vanilla JS, not Alpine — Alpine remains a reasonable choice for PyFinBot's own client-side bits, just not something to assume every greentechhub-ui interactive piece already uses it.
src/pyfinbot/
├── web/
│ ├── routes/ # login, dashboard, stocks, transactions, import, reports
│ │ # — business logic and route wiring only
│ └── templates/ # PyFinBot-specific pages only (extend greentechhub_ui base templates)
No static/, no base layout, no navbar/card/modal/toast macros — those are import greentechhub_ui and import greentechhub_fastapi, per the integration patterns documented in each package's own docs/.
v1 spent significant design effort on JWT-cookie handling and CSRF because it assumed PyFinBot would own that logic. It won't — but the owner is greentechhub-fastapi, not greentechhub-core as v2 claimed. Verified directly against current source (greentechhub-fastapi/src/greentechhub_fastapi/{registration/auth.py,auth/local.py,auth/forward_auth.py,auth/cookies.py} and greentechhub-core/src/greentechhub_core/identity/{provider.py,models.py}) — greentechhub-core has no auth module at all; auth-adjacent work there is filed under identity (the IdentityProvider protocol, DevelopmentIdentityProvider, AuthentikIdentityProvider) and security (a raw CSRF-token-generation primitive only). The real adapter-selection layer — what v2 called LocalAuthAdapter/AuthAdapter — is greentechhub-fastapi's register_auth:
# greentechhub_fastapi/registration/auth.py (real, current, shipped as of v0.5)
def register_auth(app: FastAPI, settings: GTHBaseSettings) -> None:
adapter = read_str_setting(settings, "AUTH_ADAPTER", "local")
if adapter == "local":
provider = DevelopmentIdentityProvider(secret_key=settings.secret_key)
app.dependency_overrides[get_current_user] = build_local_get_current_user(provider)
elif adapter == "forward_auth":
provider = AuthentikIdentityProvider()
app.dependency_overrides[get_current_user] = build_forward_auth_get_current_user(provider)- Now:
AUTH_ADAPTER=local(the default) —register_authbuilds aDevelopmentIdentityProviderand rebindsDepends(get_current_user)(fromgreentechhub_fastapi.auth.dependency) to resolve it from an httpOnly session cookie (gth_session, set viagreentechhub_fastapi.auth.cookies.create_session_cookie). The provider verifies a locally-issued JWT (DevelopmentIdentityProvider.issue(identity), HS256, signed withsettings.secret_key) — not PyFinBot's existing bearer-token/api/auth/loginflow directly. Concretely, PyFinBot has to write its own web login route: check credentials the same wayauth_routes.pyalready does (verify_passwordagainstUser.password_hash), build agreentechhub_core.identity.Identity(subject=user.id,username=...,email=...,groups=[],claims={}), callDevelopmentIdentityProvider(secret_key=settings.secret_key).issue(identity), thencreate_session_cookie(response, token). The package deliberately ships no login/logout routes itself (docs/auth.md: "they call a service's own credential-check logic... and use this module purely to set/clear the session cookie") — this is real route code PyFinBot writes, not glue. It runs alongside, not instead of, the existing bearer-tokenPOST /api/auth/login(API clients are unaffected). - Later:
AUTH_ADAPTER=forward_auth— once Caddy fronts PyFinBot with an Authentik outpost (goauthentik.io, perGreenMachine582/Homelab), the adapter swap is a config change, and it's already built and shipped (v0.5,AuthentikIdentityProvider+build_forward_auth_get_current_user), not future work to be written when the time comes. One real prerequisite:register_coremust also be called, with the Authentik outpost's address inTRUSTED_PROXIES— the forward-auth trust decision is made byProxyHeadersMiddleware(which onlyregister_coreinstalls), exposed asrequest.state.trusted_proxy; skippingregister_coredoesn't create a security hole, it just fails closed (nothing ever resolves). Only the OIDC-token path (validatingX-authentik-jwtagainst Authentik's JWKS) is still deferred upstream (greentechhub-corev0.5.1, needs a live Authentik instance) — the forward-auth header path PyFinBot would actually use is done. - SSO (GitHub/Google): unchanged from v2 — not PyFinBot's concern once Authentik is the IdP; social login is an Authentik-side federation setting.
- CSRF — a real gap, not "handled elsewhere." Neither package ships CSRF middleware.
greentechhub-core.security.tokensonly provides a raw token-generation/constant-time-comparison primitive; the session cookie'ssamesite="lax"(hardcoded inauth/cookies.py, not configurable) is the only baseline protection in place. If PyFinBot's HTMX form posts need more than that, it's this app's own work to add — don't assume it's provided. - No login page exists to reuse.
greentechhub-uiships noauth/login.htmlor login component today (itstemplates//components/directories have no such file — confirmed by listing both). §6 below reflects this: PyFinBot builds the whole login page, not just branding over a shared template. - A required settings change:
register_auth/register_corereadsettings.secret_key(lowercase) directly, andgreentechhub_core.config.GTHBaseSettings(the base class every service'sSettingsis meant to extend) declaressecret_key: stras a required field. PyFinBot's currentSettings.SECRET_KEYis uppercase and auto-generates an ephemeral random default. Cleanest fix: haveSettingsextendGTHBaseSettingsdirectly rather thanpydantic_settings.BaseSettings— env-var matching is case-insensitive there, so the existingSECRET_KEYenv var still resolves it, no.envchanges needed. - The one thing worth over-engineering slightly, carried over unchanged from v2: make sure no route handler imports anything from
greentechhub_fastapi.auth.localor.forward_authdirectly — onlyDepends(get_current_user). That discipline is what makes the swap free.
| Page | Notes |
|---|---|
| Login | No shared template exists yet (greentechhub-ui has no auth/login.html — confirmed against current source). PyFinBot builds the page and posts to its own new session-cookie login route (§5), extending greentechhub_ui's base app.html shell for branding/nav only |
| Dashboard | gth-stat-card summary tiles (holdings value, YTD gain/loss, total dividends received) + Grafana iframe placeholder (§8) |
| Stocks | gth-table (filter/sort on symbol, market, name, active status); gth-modal create/edit form; market sync action |
| Transactions | gth-table (filter by stock, type, date range, FY) + gth-pagination; gth-modal create/edit form; stock field is a searchable dropdown against Stock.search, not a raw ID input |
| Import | File upload (CSV/Excel), HTMX-submitted; result table with per-row validation errors; gth-toast on completion |
| Emails | Manual "Sync Commsec emails" trigger (POST /api/emails/sync-commsec) with a gth-toast result summary, mirroring BottleBot's manual scrape-trigger pattern — not in v2, added here since the router already exists |
| Dividends | Manual "Sync dividends" trigger (POST /api/dividends/sync, optional per-stock) + gth-toast — not in v2, added here since the router already exists |
| Reports | Holdings snapshot (as-of date picker), FY capital-gains report, and FY dividend-income report (GET /api/reports/dividends) — the last of these existed in the backend but was missing from v2's page list entirely — all gth-table, export-to-CSV |
- Tables:
gth-tablerenders resolved relationships server-side (e.g.AAPL · NASDAQinstead ofstock_id=17) — the API already exposes this viaTransaction.stock, just needs to be in the template context. - Forms:
gth-form's FK-field pattern is a searchable<select>(HTMX-powered, hittingStock.searchon keystroke) instead of a raw numeric input. user_idstays implicit — always the authenticated user, per the API's existing ownership model — never an editable field, consistent with how the API already derives it from the JWT rather than trusting client input.
Homelab already runs Grafana (homelab-observe node, internal URL http://grafana.homelab.local:3000). Plan for now:
- Dashboard page embeds a Grafana panel/dashboard via
<iframe>, pointed at a Grafana URL (panel ID +kioskmode) — placeholder only until real panels exist. - Dependencies to resolve before this works, not blocking initial build: Grafana
allow_embedding = trueandX-Frame-Options/CSP config (sits behind Caddy on the homelab edge); viewer auth for the embedded panel (Grafana anonymous viewer scoped to LAN/Tailscale now, or a shared Authentik OIDC session later so the iframe doesn't prompt separately); network reachability tohomelab-observe. - Until real panels exist, ship the dashboard with
gth-stat-cardsummaries and an empty/"coming soon" iframe slot.
- Authentik as IdP: covered in §5 — the adapter swap is designed in at the
greentechhub-fastapi/greentechhub-corelevel (not re-solved per service), and theforward_authside is already shipped, not just designed. Notegreentechhub-core's own README currently carries a "Status: Hold" badge — it's paused pending its adapter packages consuming it, which has already happened (greentechhub-fastapiis built on itsidentity/config/securitymodules today), so this isn't a blocker for PyFinBot, just worth knowing the upstream package considers itself between milestones. - Deployment target: Homelab's
homelab-svc-02node is earmarked for "user-facing application workloads" (currently "Planned"). ExistingDockerfile/docker-compose.ymlshould work as-is or with minor env additions once that node is active. - PyFinBot is not the only consumer: BottleBot's existing FastAPI+HTMX+Bootstrap web UI is the retrofit case for both shared packages; PyFinBot is the greenfield case. Building both roughly in parallel is what actually validates the packages are reusable rather than PyFinBot-shaped.
- RBAC and JWT revocation remain unbuilt anywhere in the ecosystem today — not tracked as a blocking dependency, just not yet PyFinBot's to solve either.
- CORS/env-mode is
greentechhub-fastapi'sregister_core, notgreentechhub-core(correcting v2, which named the wrong package):register_core(app, settings)wires request-ID, timing, security-header, CORS, and trusted-proxy middleware in one call, readingCORS_ALLOWED_ORIGINS(a list setting). This is a real, concrete duplication worth flagging: PyFinBot already built its ownCORS_ORIGINS/ENVIRONMENTsettings + manualCORSMiddlewareregistration independently (seetodo.md) — the env var name doesn't even match (CORS_ORIGINSvs.CORS_ALLOWED_ORIGINS). Adoptingregister_coreinstead, at the same time as wiringregister_auth, is a genuine follow-up consolidation — the same shape as BottleBot'sregister_healthadoption — not just a nice-to-have. It's also a hard prerequisite for theforward_authswap (§5):register_coreis what installsProxyHeadersMiddlewareand readsTRUSTED_PROXIES. - No behavior change to the existing
/api— the web layer is additive.
v2 sequenced this against greentechhub-core/greentechhub-ui's planned phasing ("wait for v0.1–v0.3"). Both packages have shipped real tagged releases since (greentechhub-fastapi v0.5.0, greentechhub-ui v0.6.0) — there's nothing left to wait for, so this is now a straight build sequence:
- Dependencies + registration: pin
greentechhub-fastapiandgreentechhub-uiasgit+https://github.com/GreenMachine582/greentechhub-{fastapi,ui}.git@vX.Y.0dependencies — this exact pattern was just proven end-to-end on BottleBot (built, tested, verified live), so start pinned rather than with local editable installs the way BottleBot originally did and later had to migrate off. HaveSettingsextendGTHBaseSettings(§5), addAUTH_ADAPTER, and callregister_core+register_authinpyfinbot.py. - Login + base shell: build PyFinBot's own login route (§5 — no shared template/adapter route exists to reuse) and base
web/templates/extendinggreentechhub_ui'sapp.html. - Core CRUD: Stocks and Transactions via
gth-table/gth-modal/gth-form. - Import: upload page,
gth-toaston completion. - Emails + Dividends: manual sync-trigger pages (§6) — small, since both are just a button +
gth-toastover an existing endpoint. - Reports: holdings snapshot, capital-gains, and dividend-income views.
- Dashboard placeholder:
gth-stat-cards + empty Grafana iframe slot. - Later: swap
AUTH_ADAPTERtoforward_authonce Authentik is live (setTRUSTED_PROXIESviaregister_corefirst — see §5); wire real Grafana panels once embedding is configured.
Two of v2's three "open" decisions are effectively resolved now by precedent, not still open:
Pinned pre-1.0 vs. wait for v1.0— resolved: pin now (§11). BottleBot already did exactly this today against these exact tagged versions and it works cleanly end-to-end; there's no longer a hypothetical to weigh.Static asset hosting (bundled-per-service vs. shared— resolved: bundled-per-service. BottleBot's actual implementation settled this in practice —static.green-tech-hub.com)greentechhub_ui.static_pathmounted at/gth-assetsvia FastAPI'sStaticFiles, no shared static host built or needed. PyFinBot should follow the same mount pattern.- Still genuinely open: whether the web UI ships in the same FastAPI process/Dockerfile as the API, or as a separate service behind Caddy — recommend same process for now given single-user scale; no new information changes this call.