auth/gateway, auth/users, and test/example-service were referencing
images without a registry prefix, causing k8s to fall back to Docker Hub
(which doesn't have these images).
Also generalises the gitea-registry imagePullSecret to all app namespaces
(auth, finance, home, test) via a for_each in Terraform.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The endpoint GET /api/v1/admin/runners/registration-token returns the
token — POST returns 405. Bootstrapper was silently failing, leaving
the secret empty and the act-runner unable to register.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace var.gitea_admin_password with random_password (like Grafana)
- Replace var.gitea_runner_token with terraform_data bootstrapper that
calls the Gitea admin API after first deploy and patches the secret
- Empty variables.tf — no manual secrets needed on terraform apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes 6 pods (3x postgresql-ha, 1x pgpool, 2x valkey-cluster) in favour
of SQLite (database) and leveldb queue, memory cache/session. Appropriate
for a single-user homelab instance with no HA requirements.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Chart 10.x pinned bitnami/redis-cluster:7.2.3-debian-11 and
bitnami/postgresql-repmgr:16.1.0-debian-11 — both removed from
Docker Hub by Bitnami. Chart 12.x replaces Redis with Valkey and
uses bitnamilegacy/ images that are still available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Terraform quoted strings $var is literal — only ${var} triggers
interpolation. The $$ was passing through as literal $$kube_* to
Fluent Bit, causing a record accessor syntax error on startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two targeted observability improvements across all homelab services.
pkg/logger/access.go (new)
HTTP access log middleware that logs one structured line per request:
method, path, status, ms, trace_id
The trace_id comes from the OTel span already in context (created by
trace.Middleware which runs outside this one), so each log entry in
Loki has a clickable link into Jaeger. Health/metrics endpoints are
excluded to avoid noise. Level is ERROR for 5xx, WARN for 4xx, INFO
otherwise.
pkg/setup/setup.go
Wire the new middleware between trace.Middleware (which creates the
span) and metrics.Middleware:
trace → AccessMiddleware → metrics → mux
Order matters: span must exist before AccessMiddleware reads it.
infrastructure/terraform/monitoring.tf
Fluent Bit was shipping all container logs to Loki with a single
static label (job=fluent-bit), making it impossible to filter logs
by service. Added a `nest/lift` filter that flattens the kubernetes
metadata block to top-level fields (kube_namespace_name,
kube_container_name, …), then promoted those as Loki label_keys.
After this change you can query:
{kube_namespace_name="finance"} |= "trace_id"
and LogQL will only return finance-api logs.
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The integration tests (testcontainers + mongo:7) exposed three real bugs:
1. deleteAllUserData filtered with bson.ObjectID on collections that store
user_id as a plain string (Account, Goal, Property, etc.) — none of them
were actually deleted. Fixed by using the original string userID for those
collections; only finance_sessions (AuthSession.UserID is ObjectID) keeps
the ObjectID filter.
2. consumeInvite correctly sets used_at, but the test was calling
getInviteByToken afterwards and expecting the invite back — that query
intentionally excludes used invites ($exists: false). Fixed the assertion
to check that the token is no longer redeemable (nil return = correct).
3. createEvent stored GoalItems as null when the slice was nil; subsequent
$push on a null field fails in MongoDB. Fixed by initialising GoalItems
to []EventGoal{} before insert so the field is always an array.
Combined unit + integration coverage: 64.7% → 79.8%
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* infra(terraform): manage finance session secret via random_password
Replace the hand-rolled variable (with insecure hardcoded default) with a
random_password resource so Terraform auto-generates a 48-char secret and
owns the finance-api-secrets k8s Secret lifecycle.
To rotate: terraform taint random_password.finance_session_secret && terraform apply
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(finance): active sessions panel + account deletion with full data purge
Sessions panel (/account):
- AuthSession now stores IPAddress and Device (browser + OS hint)
populated from X-Forwarded-For / User-Agent on every login
- Lists all active sessions with device icon, IP, sign-in time
- Current session badge ("This device") — cannot be self-revoked
- DELETE /sessions/:id revokes any other session (user-scoped)
Account deletion (POST /account/delete):
- Password accounts require password confirmation
- OAuth accounts require typing email address to confirm
- deleteAllUserData purges all 12 finance collections + user record
in a single call: accounts, categories, transactions, trades,
ticker_mappings, goals, import_schedules, properties, loans,
permissions, households, sessions → then the user itself
- Clears session cookie and redirects to login with success message
Infrastructure:
- findAuthUserByID added to store + storeIface
- getSessionsByUserID, deleteSessionForUser added to store + storeIface
- contains() added to template FuncMap
- accountTmpl registered; GET /account, POST /account/delete,
DELETE /sessions/:id routes wired
- 🔐 nav icon links to /account page
- Full EN + PT i18n coverage for all new strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(finance): expand unit test coverage from ~55% to 64.7%
- Add handler_coverage_test.go (~3300 lines) covering auth flows,
org request lifecycle, CSV bank import, property/loan views,
fiscal year operations, session management, and cross-handler
consistency (values shown on one page match actions on others)
- Add handler_org_test.go (~1800 lines) covering the full org
handler surface: teams, members, invites, events, budget lines,
tx requests (all status transitions), ledger, analysis, and reports
- Extend handler_test.go mockStore with: properties/loans slice fields,
authUsers map with session-aware lookup, household field, org maps,
and updateFiscalYearStatusErr for error-path testing
- Fix nav bar: Business and Account links now show active state and
use i18n keys (removes hardcoded emoji); add account key to en/pt locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added [help.*] sections to en.toml and pt.toml covering all six
tooltip popups (free_cash, savings_rate, net_worth, monthly_needed,
at_current_rate, disposable_after) with title, body, and formula keys
- Added step1/2/3 keys to [goals.empty] in both locales
- Added empty_state_title/subtitle and empty_step1-3 keys to
[transactions.table] in both locales
- Updated dashboard.html, goals.html, transactions.html to use T.Get
for all previously hardcoded English strings in tips and empty states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Waterfall now drills down: click Income/Living/Goals to expand
category breakdown, click a category to see its transactions
- Goal contributions are now transaction-backed (GoalID on Transaction,
SavedCents derived from MongoDB aggregation)
- Dashboard goals widget shows this-month funding status per goal
- Goals page lists funding history transactions per goal
- Transactions modal accepts a goal pre-selection (?fund_goal=<id>)
- Categories can auto-tag a linked goal on expense creation
- Settings → categories shows linked goal column and edit modal
- Free cash "what now?" section lists underfunded committed goals
with shortfall and Fund → links; shows success state when all met
- i18n: full EN/PT coverage for all new keys
- Seed data includes goal-tagged transactions so progress is non-zero
- Bug fixes: ImpactOnDisposable double-subtraction, avgMonthlySavings
denominator using only positive-savings months, cross-year month key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each committed goal card now shows a "This month" section beneath the
overall progress bar with three states:
- ✓ On track (green) when funded >= monthly need
- partial (amber) showing shortfall + "Fund it →" link
- unfunded (red) with monthly amount needed + "Fund it →" link
"Fund it →" deep-links to /transactions?fund_goal=<id> which auto-opens
the Add Transaction modal with the goal pre-selected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Goals are now funded entirely through tagged transactions — no more
manually-maintained saved_cents. Free cash waterfall (income → living →
goals → free cash) is the single source of truth for where money goes.
Core changes:
- Transaction.GoalID field links outflows to goals; SavedCents is derived
via MongoDB aggregation (getGoalFundedCentsAll) instead of stored
- Waterfall on dashboard and goals page splits outflows into living vs
goal-funded using GoalID presence
- ImpactOnDisposable fixed: uses income−living−monthlyCents instead of
waterfallFreeCash−monthlyCents (was double-subtracting goal spend)
- avgMonthlySavings fixed: divides by positive-saving months only, and
uses year+month key to avoid Dec cross-year collision
Interactive waterfall drill-down:
- Click Income / Living / Goals rows to expand category breakdown
- Click a category to reveal individual transactions inline
- All rendered server-side (instant, no extra API call)
- New WaterfallRow type + IncomeCats/LivingCats/IncomeCatTxns/LivingCatTxns
on DashboardData
Goals page:
- Summary cards switched from heuristic disposable/committed to waterfall
- Each goal card shows funding history (last 5 tagged transactions)
- "Fund this goal" button links to /transactions?fund_goal=<id>
Transactions page:
- Add Transaction modal has goal picker dropdown
- submitAdd() includes goal_id in POST body
- Auto-opens modal pre-selected when arriving from goals page
Seed:
- seedGoalTransactions() back-fills tagged contributions for all 4 demo
goals (Emergency fund, House down payment, Japan trip, MacBook Pro)
- Idempotent — skips if goal-tagged transactions already exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same TOML duplicate-key pattern as nav.analysis: the scalar
settings = "..." in [nav] blocked parsing of the [nav.settings]
sub-table. Removed the scalar; nav dropdown labels now reference
the existing nav.drawer.*_label keys which hold the same strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove conflicting `analysis = "..."` scalar from [nav] in both
en.toml and pt.toml; it shadowed the [nav.analysis] sub-table,
causing the TOML parser to reject the entire file at startup
- Update nav analysis dropdown label to reuse nav.drawer.analysis_label
- Add lang field to Translator and expose T.Lang() method so base.html
can highlight the active language in the switcher without requiring
a Lang field on every page data struct
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire T translator into auth login, auth register, and homepage
handlers; convert all hardcoded strings in those three templates
to T.Get keys (business features section, mock screen data,
sign-in block, footer, page title). Completes full i18n coverage
across all Finance Hub templates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a full translation layer (English + European Portuguese) using
BurntSushi/toml with go:embed. Locale detection reads the lang cookie,
falls back to Accept-Language, then defaults to "en". A language switcher
in the nav writes the cookie and redirects back. All 20 personal finance
templates now use {{.T.Get "key"}} for every UI string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(dashboard): committed goals widget
Shows all committed goals on the dashboard with progress bars,
months remaining, saved vs target, and monthly required (green
when on track, red when not). Links to /goals for the full view.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(auth): enable TLS on ingress so Secure session cookie is honoured
BASE_URL was https:// but the ingress had no TLS block, causing the
browser to silently drop the Secure cookie after login. Adding tls: to
the Traefik ingress makes the site serve HTTPS via Traefik's default
cert so cookie and scheme match.
Also adds SeedExtras to seed goals and property/loan data independently
of the transaction-based idempotency guard in SeedAdmin.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(property): Layer 3 — Dream House Simulator
Add /dream page with a four-phase simulation engine:
Phase 1 — Save the down payment (uses current property equity)
Phase 2 — Construction period (both loans running simultaneously)
Phase 3 — Sell current house, apply proceeds to construction loan
Phase 4 — Final state: just the construction loan remaining
Inputs: dream cost, down payment %, construction loan rate/term,
build duration, monthly savings, expected sale price. All pre-filled
from existing property/loan data when available.
Output: per-phase timeline cards, monthly cost bar chart, total
interest, final payoff date, and a key levers section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(plan): rename Dream House to Goal Planner at /plan
- Route /dream → /plan
- Nav label "Dream House" → "Goal Planner"
- Template dream.html → plan.html
- All user-facing labels generalised (construction loan → new loan,
build duration → acquisition/build period, current property →
current asset, dream house cost → new goal cost, etc.)
- Empty state updated with generic copy and 🎯 icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(goals): merge Goal Planner into /goals as a second tab
- /goals now has two tabs: "Committed goals" and "Goal Planner"
- Goal creation only happens from the Planner tab (simulate first,
then "Save as goal" → creates an uncommitted goal)
- Commitment, deadline adjustment, and deletion stay on the Goals tab
- Off-track goals show an "Adjust deadline →" button that pushes the
deadline to the realistic date based on current savings rate
- /plan and /dream both redirect to /goals?tab=planner (301)
- "Goal Planner" nav link removed; plan.html kept for redirect compat
- GoalsData gains Tab, PlanProperties, PlanLoans, HasPlanResult,
PlanResult, PlanForm fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(goals): type-driven planner — Save for a purchase vs Sell & upgrade
Goal Planner tab now opens with two goal type cards:
🛒 Save for a purchase — name, target, monthly savings, optional
deadline. Shows time-to-reach at current rate, monthly needed
to hit the deadline, and a feasibility banner.
🔄 Sell & upgrade — the full four-phase transition simulator
(existing asset + loan → acquire new → sell old → payoff).
Each type has its own focused form and result section. Selecting a
type highlights the card and loads the matching form. Results include
a "Save as goal" action that drops an uncommitted goal into the
Goals tab.
Also adds runPurchaseSim() and PurchaseSimResult model.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(finance): Layer 2 — property equity flows into Net Worth
- NetWorthData gains PropertyValueCents, LoanBalanceCents, PropertyEquityCents
- NetWorth handler fetches properties + loans; adds equity to current snapshot
and uses amortisation formula to compute historical loan balances per month,
so the chart reflects how equity grew as loans were paid down
- Dashboard NetWorthCents now includes property equity
- loanBalanceAt() helper: B_n = P*(1+r)^n - (M/r)*((1+r)^n - 1)
- networth.html: inline breakdown row in hero card (cash / portfolio / equity),
new "Property equity" breakdown card (value − loans), chart gains a dashed
red "Loans outstanding" line when properties are present
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(property): resolve template, image pull, and build issues
- Fix parseTmpl missing base.html causing "base.html is undefined" error
- Change imagePullPolicy to IfNotPresent for local k3d dev workflow
- Add SERVICE_NAME to Makefile so make build-deploy uses correct image name
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces properties and loans as first-class financial entities:
- models_property.go: Property, Loan, LoanView, PropertyView, PropertyData
- store_property.go: full CRUD for finance_properties + finance_loans collections
- handler_property.go: GET/POST /property with add/edit/delete for both entities;
amortization helpers (EMI, remaining months, total interest)
- templates/property.html: summary equity cards, property cards with equity bar
and linked loan details, standalone loan cards with payoff progress
- base.html: "Property" nav link added to desktop and mobile drawer
- storeIface + mockStore updated with 10 new property/loan methods
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): expose / without auth so homepage is publicly reachable
Adds a second Ingress (api-public) for the exact path / with no
forward-auth middleware. Traefik prefers the Exact match for the root,
while the Prefix ingress (with auth) still protects all other routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: homepage renders correctly at / for unauthenticated visitors
Two fixes:
1. Added parseStandalone() helper — parseTmpl() roots on "" but ParseFS()
stores standalone (no {{define}}) files under their base filename, so
Execute() ran the empty root and returned Content-Length: 0.
2. Added router.priority: 100 annotation to api-public ingress so Traefik
picks the Exact / rule over the Prefix / rule (Traefik ranks by rule
string length by default, which made PathPrefix beat Path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): remove forward-auth middleware from finance ingress
The app now handles its own auth at /auth/login — Traefik no longer
needs to forward-auth requests, which was causing redirects to
auth.homelab.local instead of finance.homelab.local.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): harden authentication for cloud deployment
1. Secure cookie flag — set when BASE_URL starts with https://
2. SameSite=Strict on session cookie (was Lax)
3. Rate limiter — per-IP, 10 failures → 15-min lockout, auto-cleanup goroutine
4. Session rotation on login — old session deleted before issuing new one
(prevents session fixation attacks)
5. bcrypt cost 12 (was DefaultCost/10, OWASP minimum for cloud)
6. Security headers middleware on all responses:
X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, Content-Security-Policy, HSTS (when HTTPS)
7. Structured audit logging — login success/failure/lockout with IP + email
8. Google OAuth state cookie gets Secure flag too
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(infra): Gitea self-hosted CI/CD + MongoDB PVC + registry pipeline
- Add Gitea Helm deployment (git hosting, container registry, Gitea Actions)
- Add act runner with DinD sidecar for Docker builds in-cluster
- Add RBAC so act runner can kubectl-deploy to finance namespace
- Fix MongoDB StatefulSet: add volumeClaimTemplates (data was lost on restart)
- Configure k3d containerd to mirror git.homelab.local → Gitea NodePort 30002
- Add .gitea/workflows/finance-api.yml: test → build/push → rolling deploy
- Update finance-api deployment: Gitea registry image, imagePullPolicy Always
- Extract finance-api secrets (SESSION_SECRET, Google OAuth) into Terraform
- Add variables.tf for Gitea admin password and runner token
All changes testable on local k3d before the VPS exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): expose / without auth so homepage is publicly reachable
Adds a second Ingress (api-public) for the exact path / with no
forward-auth middleware. Traefik prefers the Exact match for the root,
while the Prefix ingress (with auth) still protects all other routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: homepage renders correctly at / for unauthenticated visitors
Two fixes:
1. Added parseStandalone() helper — parseTmpl() roots on "" but ParseFS()
stores standalone (no {{define}}) files under their base filename, so
Execute() ran the empty root and returned Content-Length: 0.
2. Added router.priority: 100 annotation to api-public ingress so Traefik
picks the Exact / rule over the Prefix / rule (Traefik ranks by rule
string length by default, which made PathPrefix beat Path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): remove forward-auth middleware from finance ingress
The app now handles its own auth at /auth/login — Traefik no longer
needs to forward-auth requests, which was causing redirects to
auth.homelab.local instead of finance.homelab.local.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(auth): harden authentication for cloud deployment
1. Secure cookie flag — set when BASE_URL starts with https://
2. SameSite=Strict on session cookie (was Lax)
3. Rate limiter — per-IP, 10 failures → 15-min lockout, auto-cleanup goroutine
4. Session rotation on login — old session deleted before issuing new one
(prevents session fixation attacks)
5. bcrypt cost 12 (was DefaultCost/10, OWASP minimum for cloud)
6. Security headers middleware on all responses:
X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, Content-Security-Policy, HSTS (when HTTPS)
7. Structured audit logging — login success/failure/lockout with IP + email
8. Google OAuth state cookie gets Secure flag too
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: public landing page with auth-conditional state
Rewrites homepage.html as a full marketing landing page serving both
unauthenticated visitors (Sign In CTA) and authenticated users (Personal
+ Business portal links). Fixes handler to pass UserID so auth-conditional
rendering activates correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): expose / without auth so homepage is publicly reachable
Adds a second Ingress (api-public) for the exact path / with no
forward-auth middleware. Traefik prefers the Exact match for the root,
while the Prefix ingress (with auth) still protects all other routes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: homepage renders correctly at / for unauthenticated visitors
Two fixes:
1. Added parseStandalone() helper — parseTmpl() roots on "" but ParseFS()
stores standalone (no {{define}}) files under their base filename, so
Execute() ran the empty root and returned Content-Length: 0.
2. Added router.priority: 100 annotation to api-public ingress so Traefik
picks the Exact / rule over the Prefix / rule (Traefik ranks by rule
string length by default, which made PathPrefix beat Path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: self-contained auth — email/password + Google OAuth, HMAC session cookies
Embeds a full authentication system into the finance API so it can be
deployed as a standalone container without any external auth dependency.
- Email/password registration and login with bcrypt hashing
- Google OAuth 2.0 (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET env vars)
- HMAC-SHA256 signed session cookies (SESSION_SECRET env var, 30-day TTL)
- Sessions stored in MongoDB finance_sessions with TTL index auto-expiry
- Users stored in MongoDB finance_users with unique email index
- /auth/login, /auth/register, /auth/logout, /auth/oauth/google routes
- authMW now redirects to /auth/login?next=... instead of auth.homelab.local
- getAuth() resolves session cookie first, falls back to X-Auth-* headers
- Default categories seeded automatically on new account creation
- seed.go checks finance_users before the shared legacy users collection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: homepage sign-in links point to /auth/login instead of auth.homelab.local
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(k8s): remove forward-auth middleware from finance ingress
The app now handles its own auth at /auth/login — Traefik no longer
needs to forward-auth requests, which was causing redirects to
auth.homelab.local instead of finance.homelab.local.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- New animated homepage at / with 3D card tilt, particle canvas,
floating ring decorations, and gradient title shimmer
- Personal finance pages move to /dashboard (base.html shows only
personal nav + a subtle Business link)
- Business/org inner pages use base_org.html with a purple theme,
org breadcrumb, Year/Team dropdowns, and a ← Hub back link
- org home/teams/members/invite/events/requests/ledger/analysis/report
all switched to renderOrg() + base_org.html
- Route strings updated to org-home, org-teams, org-events, etc.
so active nav highlighting works correctly in the business shell
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Goals are stored as EventGoal items embedded in the event document.
During active fiscal years, members can check/uncheck goals inline.
Goals can be added and deleted while the event is not yet approved.
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Form inputs:
- Add a catch-all CSS rule targeting all text-type inputs, selects, and
textareas not already covered by .form-group or .form-input — sets
background:--bg2, color:--text, focus ring. Fixes white-box appearance
in transactions, goals, settings, portfolio, import, people, and tax
templates without touching any HTML.
Team avatars:
- OrgTeam.Avatar string (emoji) — persisted in MongoDB, defaults to 👥
- OrgTeamCreate handler reads "avatar" form field
- org_teams.html: emoji picker (30 options) in new-team modal; preview
updates live; selected emoji highlighted with accent border
- avatarEmojis() and teamAvatar() template functions registered in FuncMap
- Team badges in org_members, org_events, org_event_detail, org_report
all show the emoji inline with the team name
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Org templates use class="form-input" and class="form-label" which had no
CSS definition — browsers fell back to white-box defaults, clashing with
the dark teal theme.
- .form-input: dark bg (--bg2), teal border, accent focus ring + glow,
placeholder dimming, file input button styling
- .form-label: uppercase micro-label style (11px, --text3) matching the
existing card section headers throughout the app
- textarea.form-input: resize:vertical, min-height, line-height
- select option / .form-input option: dark background on dropdown items
- Keep .form-group label unchanged so existing non-org templates are unaffected
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- org_home.html: fix Requests/Ledger links (were pointing to non-existent
/years/{id}/requests routes; corrected to /orgs/{slug}/requests and
/orgs/{slug}/ledger). Add Analysis link. Add Close year button for admins.
- OrgRequestNew: pass events + teams to GET template so dropdowns are
populated (were silently discarded with _ = events).
- OrgRequestDetailData: add NewEvents/NewTeams fields for new-request form.
- OrgRequestUpload: implement file upload handler — saves to
/data/org-files/{org_id}/{req_id}/{id} and records metadata in MongoDB.
Register POST /orgs/{slug}/requests/{req_id}/upload route.
- org_request_detail.html: show upload form in attachments section;
populate event/team selects on new request form.
Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds multi-tenant organisation support inside the existing finance namespace.
Users can create organisations, invite others via a copy-paste token link,
and manage teams/members with RBAC (admin, finance, member, viewer).
Fiscal year lifecycle is gated: activation requires all planned events to
be approved first. All org data lives in `org_`-prefixed MongoDB collections.
New files:
- models_org.go — domain types (Org, OrgTeam, OrgMember, OrgInvite,
FiscalYear, OrgEvent, BudgetLine, EventComment,
TxRequest with full StatusLog audit trail, etc.)
- store_org.go — MongoDB store methods for all org collections
- handler_org.go — HTTP handlers + RegisterOrgRoutes(); join invite
route lives at /join/{token} to avoid ServeMux
conflict with /orgs/{slug}/... wildcard routes
- templates/org_*.html — list, create, home, teams, members, invite, join
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove erroneous /100 in currentValue calculation (portfolio values
were 100x too small, causing net worth card to show ~€0)
- Add User-Agent header to Yahoo Finance requests (avoids 429s)
- Wrap ES module chart body in if(total>0){} block (return at top
level of a module is a SyntaxError — chart was silently broken)
- Add mobile hamburger menu: full-screen drawer at ≤720px with
animated open/close, all nav links, scroll lock while open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Provides the 5-row Trade Republic securities CSV that
TestParseSecuritiesCSV_FromFile expects; CI was failing
with "no such file or directory".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace indigo accent palette with deep black backgrounds and
cyan-teal accents (#00c9b8 dark, #00897b light). Borders, glows,
text muted tones and background gradients all updated to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a holding has no price (ISIN not in the built-in map and Yahoo
rejects the raw ISIN), the portfolio page now shows an amber banner
listing each missing ISIN with an inline text input and a "Look up"
link to Yahoo Finance symbol search.
Submitting the form POSTs to /portfolio/ticker which upserts the mapping
into a finance_ticker_mappings collection keyed by (user_id, ISIN).
On the next page load custom mappings are resolved first, before the
hardcoded isinToTicker table, so user overrides always win.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`return` at the top level of a <script type="module"> is a SyntaxError
in strict mode, so the entire Three.js chart script was killed before
executing regardless of whether prices were available.
Replaced `if (total <= 0) return` with `if (total > 0) { ... }` wrapping
the full chart body. Also filter out holdings with value=0 before building
the arc geometry so a single un-priced ISIN can't produce a zero-arc slice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without it the API returns "Too Many Requests" (not JSON), prices map
stays empty, currentValue = 0, and every holding shows -100% P&L with
an empty allocation chart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
currentPrice is already in cents, so currentValue = price * shares.
The extra /100 made every holding appear worth 100x less than its cost,
producing ~-100% P&L on every position and an empty allocation chart
(values too small for Chart.js to render).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Merge Sharing + Household → /people (tab switcher: sharing | household)
- Merge Accounts + Categories → /settings (tab switcher: accounts | categories)
- Add Analysis dropdown in nav: Reports, Projections, Tax, Net Worth, What If
- Add Settings dropdown in nav: Accounts & Categories, Import CSV, Import Guide
- Legacy GET /sharing, /household, /accounts, /categories redirect 301 to new URLs
- Remove Import and Import Guide as standalone nav links
- New People handler consolidates all people-related mutations (_action field)
- New Settings handler renders both account and category lists in one page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auto Import page replaced with a static Import Guide: step-by-step
instructions for CGD/Trade Republic/generic CSV export, duplicate
detection explanation, and a curl example for headless automation
- Removed POST /auto-import and DELETE /auto-import/{id} routes and
their handler logic; schedule CRUD was misleading since no bank
exposes an unauthenticated CSV endpoint
- Nav label changed from "Auto Import" to "Import Guide"
- Transactions page now renders an amber banner when redirected with
?notice=all_duplicates (every row in the uploaded file was skipped)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compute a sha256 fingerprint (date|description|amount|account_id, first
16 hex chars) for every CSV row and store it as bank_ref. At preview
time, existing fingerprints are fetched and matching rows are shown
greyed out with a "duplicate" label. At confirm time, those rows are
silently skipped — only truly new transactions are inserted.
If every row is a duplicate the user is redirected with ?notice=all_duplicates
instead of inserting an empty batch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>