Host mode lacks Node.js so actions/checkout@v4 fails. Switch label to
ubuntu-latest:docker:node:20 which has Node.js for JS actions. Install
Docker CLI in the deploy job since node:20 doesn't include it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .gitea/workflows/deploy.yaml: test → build ARM64 → push to Gitea
registry → kubectl set image on push to main
- Remove .github/workflows/deploy.yml (GitHub kept as test-only backup)
Requires Gitea Actions secrets: GITEA_REGISTRY_PASSWORD, KUBECONFIG_B64.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- deploy.yml: on push to main, builds linux/arm64 image, pushes to
Gitea registry, deploys via SSH kubectl set image
- ci.yml: gate to PRs targeting main only
- finance-api deployment: imagePullPolicy Always so SHA-tagged images
are always pulled on rollout
Requires GitHub Actions secrets: GITEA_REGISTRY_PASSWORD, VPS_HOST,
VPS_USER, VPS_SSH_KEY.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The k8s/*.yaml glob in each skaffold.yaml picks up servicemonitor.yaml
and fails when monitoring is disabled (CRD not installed). Moving them
to k8s/monitoring/ keeps the config but excludes them from the default
deploy. Apply manually when enable_monitoring=true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
defaultRepo is not valid inside a profile build block in v4beta13.
Pass it as a CLI flag instead and expose make deploy / make deploy-<module>
targets for convenience.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MongoDB 8.x (both 8.0 and 8.2) uses tcmalloc-google which segfaults
(exit 139) on Hetzner ARM64 kernels with transparent hugepages disabled.
MongoDB 7 LTS uses jemalloc and runs cleanly on the same hardware.
PVC was already wiped so there is no FCV incompatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mongo:8 resolves to 8.2 which uses tcmalloc-google. That allocator
segfaults (exit 139) when transparent hugepages are disabled, which is
the default on Hetzner kernels. MongoDB 8.0 LTS uses jemalloc and does
not have this issue.
PVC must be deleted before applying since FCV 8.2 data files can't be
opened by 8.0. Finance API seeds admin on startup so no data is lost.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mongo:7 can't open data files written by mongo:8 (exit code 62 =
NeedsDowngrade). Stay on mongo:8 — the SIGSEGV was caused by the
--wiredTigerCacheSizeGB=0.25 flag, not the version. Removing the flag
is the actual fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mongo:8 was crashing with exit code 139 (SIGSEGV) on the Hetzner CAX11
ARM64 instance. Switch to mongo:7 (LTS) which has more stable ARM64
support. Also remove the --wiredTigerCacheSizeGB=0.25 arg since the
512Mi memory limit already bounds memory use adequately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLite and LevelDB can't be accessed by two pods simultaneously.
RollingUpdate starts a new pod before the old one stops, causing
the queue lock to fail on startup. Recreate terminates the old
pod first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>