101 Commits

Author SHA1 Message Date
Gonçalo Rodrigues
6dd7592ac9 fix(gitea): add TLS, scheme helper, and Skaffold registry config (#41)
Changes from PR #40 that didn't make it into main:
- local.scheme derived from var.domain (http for homelab.local, https otherwise)
- Gitea ROOT_URL and runner bootstrap URLs use local.scheme
- Gitea Helm ingress gets TLS + letsencrypt certresolver annotations
- Skaffold CI profile sets defaultRepo=git.gugagr.xyz/admin

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 22:06:06 +01:00
Gonçalo Rodrigues
d4ccff518e feat: switch to gugagr.xyz with TLS via Let's Encrypt (#39)
Adds Traefik Helm release (kube-system) with ACME HTTP-01 challenge
configured for Let's Encrypt, replacing the k3s-disabled bundled Traefik.

Migrates all hostnames from *.homelab.local to *.gugagr.xyz and upgrades
all ingresses to HTTPS with certresolver=letsencrypt annotations.

Adds var.domain (default homelab.local) to Terraform so the domain is
a single config point for monitoring and Gitea ingresses.

Gateway reads DOMAIN env var at runtime — falls back to homelab.local
so local k3d dev continues to work without changes.

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 21:45:19 +01:00
Gonçalo Rodrigues
8436295bbc feat(infra): gate observability stack behind var.enable_monitoring (#38)
Adds enable_monitoring variable (default true) that controls whether
Prometheus/Grafana, Loki, Fluent Bit, and Jaeger are deployed.
Setting it to false saves ~1.5 GB RAM, making the stack viable on
a 2–4 GB VPS without touching the application services.

Also caps MongoDB WiredTiger cache at 256 MB (--wiredTigerCacheSizeGB=0.25)
so it doesn't balloon on memory-constrained hosts.

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:44:14 +01:00
Gonçalo Rodrigues
292b2f46f0 fix(finance): seed admin account in finance_users on startup (#37)
SeedAdmin now creates the finance_users account from ADMIN_EMAIL /
ADMIN_PASSWORD env vars if it doesn't exist, so a fresh cluster
bootstraps a working login without manual registration.

Wires ADMIN_EMAIL and ADMIN_PASSWORD into the deployment from the
finance-api-secrets k8s secret (optional — pod still starts without it).

Also gitignores /main build artifact and *.test binaries.

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:43:59 +01:00
Gonçalo Rodrigues
dcb573ed8a fix(auth): set cookie Domain to .homelab.local for subdomain coverage
Without the leading dot, the auth_token cookie was only sent to the
exact host homelab.local — not to finance.homelab.local, auth.homelab.local,
etc. — so the forward-auth check failed on any subdomain after login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:43:33 +01:00
Gonçalo Rodrigues
464bde2ee6 chore: update Makefiles for Skaffold-based workflow
Root Makefile:
- Replace deploy-*/deploy-all/restart-all with skaffold dev/run
- Add dev-<service> targets for per-service watch mode
- Rename dev → skaffold dev (was: up+infra+deploy-all)
- Rename to bootstrap for the full first-time setup
- Add test-integration target

service.mk:
- Remove REGISTRY variable (image is now homelab/<svc>, no registry prefix)
- Remove skaffold-gen (skaffold.yaml files are committed)
- Update skaffold-dev/run to pass -p local
- Keep build-deploy as a manual fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:34:08 +01:00
Gonçalo Rodrigues
0442f6cde7 feat: add Skaffold for local k3d development
- Root skaffold.yaml composes all services; local profile auto-activates
  on k3d-homelab context (push: false, k3d image import); ci profile
  pushes to registry with git-commit tags
- Per-service skaffold.yaml for per-service dev (run from service dir)
- Add finance-api skaffold.yaml (was missing)
- Deployment images use bare name (homelab/<svc>) — Skaffold substitutes
  the correct tagged image; no registry prefix needed for local dev
- Add namespace: auth to all auth service manifests
- Remove skaffold.yaml from .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:30:24 +01:00
Gonçalo Rodrigues
a7ba0a9dd6 refactor(infra): gate Gitea and act-runner behind var.enable_gitea
All Gitea and runner resources use count = var.enable_gitea ? 1 : 0
(or for_each with an empty set when false). The gitea namespace is
conditionally included. Default is false.

To enable: terraform apply -var enable_gitea=true

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:14:57 +01:00
Gonçalo Rodrigues
c3b7003725 chore(infra): disable Gitea and act-runner — postponed until dedicated server
Empties gitea.tf and act-runner.tf so terraform apply removes all Gitea
and runner resources. Drops the gitea namespace from the managed list.
Full config preserved in git history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 16:06:32 +01:00
Gonçalo Rodrigues
f5c08d6f02 fix: add git.homelab.local registry prefix and imagePullSecrets to all app deployments
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>
2026-06-20 16:01:55 +01:00
Gonçalo Rodrigues
e39840cca2 fix(infra): use GET not POST for Gitea runner registration token API
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>
2026-06-20 15:49:26 +01:00
Gonçalo Rodrigues
07c2dc3ecb feat(infra): auto-generate Gitea admin password and runner token
- 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>
2026-06-20 15:43:10 +01:00
Gonçalo Rodrigues
dee8b5b40a fix(infra): simplify Gitea to SQLite + in-process — drop PostgreSQL and Valkey
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>
2026-06-20 15:34:28 +01:00
Gonçalo Rodrigues
3c981b6ba4 fix(infra): bump Gitea chart 10.x → 12.x to fix ImagePullBackOff
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>
2026-06-20 15:29:37 +01:00
Gonçalo Rodrigues
079ffae90b fix(infra): remove double-dollar escape in Fluent Bit label_keys
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>
2026-06-20 15:23:46 +01:00
Gonçalo Rodrigues
99ed992d98 obs: request access log middleware + Loki label enrichment (#36)
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>
2026-06-20 15:15:06 +01:00
Gonçalo Rodrigues
40c8632c7e fix(finance): fix 3 store bugs found by integration tests; add store_integration_test.go (#35)
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>
2026-06-20 15:15:03 +01:00
Gonçalo Rodrigues
91796c9fb9 test(finance): expand unit test coverage from ~55% to 64.7% (#34)
* 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>
2026-06-20 15:07:29 +01:00
Gonçalo Rodrigues
6485f58f23 i18n(finance): translate all help tips and guided empty states
- 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>
2026-06-19 22:51:17 +01:00
Gonçalo Rodrigues
6acea3da31 feat(finance): inline help tips + guided empty states
- Global .help-tip / .help-popup CSS + click-toggle JS in base.html
- Global .setup-steps / .setup-step CSS for step-by-step guidance
- Dashboard: ? tooltips on Free Cash (formula), Savings Rate, Net Worth
- Goals: ? tooltips on Monthly Amount, At Current Rate, Free Cash After
- Goals empty state: 3-step guide (planner → commit → fund)
- Transactions empty state: 3-step guide (account → import → tag)
  with prominent Import / Add buttons replacing the inline text links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 22:43:59 +01:00
Gonçalo Rodrigues
e93cb38756 feat(finance): interactive waterfall + goal auto-tag + free cash prompt
- 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>
2026-06-19 22:35:29 +01:00
Gonçalo Rodrigues
4cfe80e3d5 feat(finance): goal monthly funding status on dashboard
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>
2026-06-19 22:27:31 +01:00
Gonçalo Rodrigues
5f60d963a0 feat(finance): transaction-backed goals + interactive waterfall
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>
2026-06-19 22:18:47 +01:00
Gonçalo Rodrigues
ccbb60ace9 fix(finance): i18n — remove nav.settings scalar conflicting with [nav.settings] table
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>
2026-06-17 22:51:25 +01:00
Gonçalo Rodrigues
7aa510e1f5 fix(finance): i18n — fix TOML duplicate key and missing Lang on Translator
- 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>
2026-06-17 22:49:54 +01:00
Gonçalo Rodrigues
2166790fab feat(finance): i18n — auth pages and homepage fully translated
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>
2026-06-17 22:42:27 +01:00
Gonçalo Rodrigues
4b7c01e632 feat(finance): i18n — TOML-based translations for all personal finance templates
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>
2026-06-17 22:32:49 +01:00
Gonçalo Rodrigues
b4b7a1381c feat(dashboard): committed goals widget (#32)
* 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>
2026-06-16 22:27:57 +01:00
Gonçalo Rodrigues
2ab3acdce2 feat(goals): Goal Planner — type-driven planner merged into /goals tab
* 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>
2026-06-16 22:02:41 +01:00
Gonçalo Rodrigues
ac073acad9 feat(finance): Layer 2 — property equity integrated into Net Worth (#30)
* 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>
2026-06-15 23:01:55 +01:00
Gonçalo Rodrigues
4305a77612 feat(finance): Layer 1 — Property & Loan foundation (#29)
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>
2026-06-15 22:40:57 +01:00
Gonçalo Rodrigues
05dd725579 feat(infra): Gitea self-hosted CI/CD + MongoDB PVC + registry pipeline (#28)
* 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>
2026-06-15 21:45:34 +01:00
Gonçalo Rodrigues
cedc0c2192 feat: self-contained auth system for standalone cloud deployment (#27)
* 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>
2026-06-15 18:30:19 +01:00
Gonçalo Rodrigues
fb6c839352 feat: public landing page + split personal/business nav (#26)
* 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>
2026-06-15 18:18:09 +01:00
Gonçalo Rodrigues
541a1c3556 feat: animated homepage + split personal/business navigation (#25)
- 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>
2026-06-14 17:02:37 +01:00
Gonçalo Rodrigues
1fce3b36aa feat: add checkable goals list to org events (#24)
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>
2026-06-14 16:15:55 +01:00
Gonçalo Rodrigues
26a7236494 style: global dark form inputs + team emoji avatars (#23)
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>
2026-06-14 16:08:29 +01:00
Gonçalo Rodrigues
ceeee2a46a style: add .form-input/.form-label CSS classes for dark-theme form fields (#22)
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>
2026-06-14 16:03:45 +01:00
Gonçalo Rodrigues
07e3525dae fix(finance): org polish — broken links, upload handler, populated selects (#21)
- 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>
2026-06-14 16:00:23 +01:00
Gonçalo Rodrigues
0f58a51c6d feat(finance): org Phases 2-5 — events, requests, ledger, analysis, report (#20)
Phase 2 — Event planning:
- Create/edit/delete/submit events per fiscal year
- Budget lines (income + expense) with planned amounts
- Admin review flow: approve / reject / comment
- Post-mortem feedback comments on closed years

Phase 3 — Transaction requests:
- 5 types: reimbursement, purchase_order, cash_advance, income, budget_transfer
- Full status machine with append-only StatusLog audit trail
- PO delivery sub-form (actual vendor, amount, invoice note)
- Cash advance settlement sub-form (spent + returned)
- Ledger view per fiscal year with income/expense summary
- Bank CSV import: parse → preview → confirm → ledger entries

Phase 4 — Plan vs actual analysis:
- Variance table by event (planned vs actual income/expense)
- Variance table by team

Phase 5 — Year-end report:
- Executive summary with net variance
- Per-event section: budget lines + team feedback comments
- Feedback form available on closed fiscal years

Supporting changes:
- New store methods: getLedgerEntries, createLedgerEntry, updateLedgerEntry,
  getAttachments, createAttachment
- New template funcs: dateInput, statusColor, varColor
- parseEuroAmount + parseBankCSV helpers in handler_org.go

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 12:58:47 +01:00
Gonçalo Rodrigues
1a935aa8ff Merge pull request #19 from GoncaloRodri/feature/org-phase1
feat(finance): org management scaffold — Phase 1
2026-06-14 12:45:04 +01:00
Gonçalo Rodrigues
6ed848a001 feat(finance): org management scaffold — Phase 1
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>
2026-06-14 12:43:48 +01:00
Gonçalo Rodrigues
1b9284801c Merge pull request #17 from GoncaloRodri/feature/fixes-mobile-nav
fix: portfolio values, Yahoo UA header, mobile hamburger nav
2026-06-13 19:37:39 +01:00
Gonçalo Rodrigues
a66941160a fix: portfolio value formula, Yahoo UA header, mobile nav hamburger
- 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>
2026-06-13 19:33:12 +01:00
Gonçalo Rodrigues
981b832d7f test: add missing traderepublic_securities.csv testdata fixture
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>
2026-06-13 19:32:17 +01:00
Gonçalo Rodrigues
b1c9609f42 Merge pull request #18 from GoncaloRodri/feature/nav-cleanup
Feature/nav cleanup
2026-06-13 19:31:12 +01:00
Gonçalo Rodrigues
09984272cd style: dark teal/cyan theme across the whole app
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>
2026-06-13 19:12:58 +01:00
Gonçalo Rodrigues
5c6752b4a3 style: blue-green palette for portfolio allocation chart
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:09:02 +01:00
Gonçalo Rodrigues
6712c36081 feat: user-editable ISIN→ticker mappings for unrecognised holdings
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>
2026-06-13 19:05:36 +01:00
Gonçalo Rodrigues
5dc920cb1a fix: add QDVE.DE ticker mapping for iShares S&P 500 IT Sector ETF (IE00B3WJKG14)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 19:00:56 +01:00