Bikontrol keeps a motorcycle's maintenance on schedule. You register your bikes, log the kilometres you ride, and get a maintenance plan (oil, chain, brakes…) that stays honest with real usage instead of a guess. Every item shows how far away it is — by km, by time, or both — and turns red before it is overdue, so nothing gets forgotten.
It is a production app: https://bikontrol.santidev21.tech — a B2C product built for riders, not a demo.
- Your motorcycles — create, edit and disable bikes (brand, year, cc, plate, photo).
- Kilometre history — log rides; the odometer drives the whole maintenance plan.
- Maintenance that knows your usage — follow predefined types (oil, chain, spark plug, brakes…) or define your own, tracked by km interval, by time (weeks), or both. Each shows % remaining and a health bucket (OK / Próximo / Crítico / Vencido) computed from the latest km and the last record.
- Record what you did — mark maintenance performed (with km and date); history feeds the plan.
- Reminders — a daily engine flags maintenance that is overdue or close (≤20 % of its life), deduped so it never nags, and emails one digest per user with everything due; the API surfaces what is due (
GET /api/reminders/due) and you can turn reminders off. Web push is next. - Statistics — fleet km, maintenance health, km per bike, records by type, 6-month activity.
- Profile — edit your name, change your password, see your app version.
- Sign in your way — email + password or "Sign in with Google".
Built to be sold, not just shown:
- Secure auth — JWT with sliding refresh-token sessions (rotated, stored hashed), per-user salt, email verification, account lockout, strict rate limiting on auth endpoints.
- Private by default — the demo tenant is opt-in; the database lives on an internal Docker network only and never on the shared one.
- Operable — liveness (
/health) vs readiness (/ready, checks PostgreSQL), structured logs with request correlation, optional Sentry, verified backups (every dump is restored into a throwaway DB before it is trusted), DB auto-migrations on deploy. - Checked on every change — backend unit + integration tests (real PostgreSQL via Testcontainers), frontend Vitest, coverage gate, SonarCloud (quality gate + new-code coverage), CodeQL, Gitleaks, Trivy, mutation testing and a migration rollback test; deploys roll back automatically if the health check fails.
| Layer | Choice |
|---|---|
| Frontend | Angular 22, SCSS, Tailwind CSS, PWA (service worker), Vitest |
| Backend | .NET 8, Clean Architecture (API/Application/Domain/Infrastructure/Persistence/Shared), EF Core, xUnit |
| Database | PostgreSQL 16 (EF Core migrations) |
| Auth | JWT + refresh tokens, Google OAuth (ID-token flow) |
| Infra | Docker Compose, nginx gateway (TLS/HSTS/CSP), GitHub Actions → VPS |
Bikontrol is served at https://bikontrol.santidev21.tech/ behind the vps-gateway reverse proxy:
Internet → gateway (nginx) → bikontrol (Angular, :8080)
→ bikontrol-api (.NET, :8080) → bikontrol-db (PostgreSQL)
bikontrol-net(external, shared with the gateway):bikontrol+bikontrol-api.bikontrol-internal-net(internal): database only. The DB is never on the shared network.
Bikontrol/
├─ Bikontrol/ # .NET solution and backend tests
├─ bikontrol-web/ # Angular application
├─ docs/ # guides (deployment, etc.) and specs (docs/specs/)
├─ scripts/ # orchestration scripts
├─ .opencode/ # AI home: agent/, command/, skills/
├─ package.json # root scripts for local dev and CI parity
└─ opencode.json # opencode config (instructions, MCP, permissions)
The database (PostgreSQL 16) always runs in Docker — loopback-only (127.0.0.1:5434), never exposed externally. Only where the app itself runs changes:
npm run docker:dev→ everything (DB + API + frontend) in Docker, closest to prod.npm run dev→ DB in Docker, API + frontend native (dotnet run/ng serve) with hot reload, against the samebikontrol_db_datavolume.
- Node.js 20+
- .NET 8 SDK
- Docker Desktop (running)
cp .env.example .env
# Fill POSTGRES_PASSWORD (avoid '$'; wrap '#' or spaces in single quotes),
# Jwt__Key (generate with: openssl rand -base64 48)npm run docker:dev
# = docker compose -f docker-compose.yml -f docker-compose.local.yml up --build- API:
http://127.0.0.1:8080(liveness at/health, readiness at/ready) - Web:
http://127.0.0.1:4200 - DB:
127.0.0.1:5434(loopback-only) - Data persists in the
bikontrol_db_datavolume;docker compose … down -vwipes it.
npm run devStarts Postgres in Docker, then runs the frontend (http://localhost:4201) and the API (http://localhost:5202, plus https://localhost:7179) together. The dev frontend uses plain HTTP so the browser never trips on the self-signed dev certificate (ERR_CERT_AUTHORITY_INVALID). On first run it creates Bikontrol/Bikontrol.API/appsettings.Development.json from the committed .example template — review ConnectionStrings:DefaultConnection (127.0.0.1:5434) and Jwt:Key.
Single side:
npm run dev:ui # Angular app only (:4201)
npm run dev:api # API only (:5202 HTTP + :7179 HTTPS)If you run Angular directly from
bikontrol-web/withnpm start, it uses the default:4200unless you pass--port 4201.
npm run db:up # start Postgres only (127.0.0.1:5434, loopback-only)
npm run db:down # stop it (data persists in the volume)
npm run db:migrate # apply EF migrations (dotnet ef database update)
# New migration:
npm run db:migration:add -- YourMigrationNameNative dotnet run takes the DB credentials and JWT key from .env, so they always match the Docker Postgres.
| Command | Purpose |
|---|---|
npm run dev |
DB (Docker) + frontend + backend with hot reload |
npm run dev:ui / npm run dev:api |
Frontend / API only |
npm run db:up / npm run db:down |
Start / stop Postgres in Docker |
npm run db:migrate |
Apply EF migrations |
npm run db:backup / db:verify-backup / db:restore |
Dump the DB / prove it restores / restore it |
npm run smoke -- <url> |
Post-deploy smoke test against a running deployment |
npm run docker:dev |
Full stack in Docker (like prod) |
npm run build |
Build frontend and backend |
npm run test |
Run frontend and backend tests |
- API project:
Bikontrol/Bikontrol.API - Persistence project:
Bikontrol/Bikontrol.Persistence - Backend solution:
Bikontrol/Bikontrol.sln
- The root
package.jsonis the orchestration layer; the frontend keeps its own Angular scripts insidebikontrol-web/package.json. - The API runner skips
launchSettings.jsonand listens onhttp://localhost:5202+https://localhost:7179(same as thehttpslaunch profile). HTTPS redirection is disabled in Development so the dev frontend can use plain HTTP and avoid untrusted-cert errors on Linux. - The root frontend runner forces
http://localhost:4201so it does not collide with the default Angular port.
| Problem | Cause | Fix |
|---|---|---|
Login returns 504 Gateway Timeout but the API is healthy |
Stale PWA service worker cached in the browser | Hard-refresh (Ctrl+Shift+R) or clear site data for localhost:4200 |
/ready returns 503 |
API up but PostgreSQL unreachable | Check the db container / npm run db:up; /health stays 200 by design |
| Users see an old version after a deploy | Normal PWA behavior | The app detects the new version and shows a "Nueva versión disponible" prompt (reload when ready). Check the served version in Perfil → Versión |
npm run dev → API auth fails against Docker Postgres |
.env POSTGRES_PASSWORD / Jwt__Key still CHANGE_ME |
Fill .env (the root scripts inject it into the API) |
AutoMapperis pinned to12.0.1. Versions>= 15require a paid license and pull .NET 9/10 +Microsoft.IdentityModel8.x dependencies that conflict with the net8.0 JWT stack. The upstream advisoryGHSA-rvv3-g6hj-g44x(DoS via deep recursive object graphs) does not apply here: Bikontrol only maps flat, fixed-shape DTOs with no recursive/self-referencing graphs reachable from user input. The advisory is suppressed inBikontrol/Directory.Build.propswith that rationale.
Deploys happen automatically on push to main via GitHub Actions. Each deploy backs up (and verifies) the DB, then healthchecks the containers and runs a post-deploy smoke test against the public origin (npm run smoke <url>); the deploy rolls back automatically if either fails. Staging uses the same script with DEPLOY_DIR/SMOKE_URL overrides. For VPS setup and manual commands, see docs/DEPLOYMENT.md.
The images live in docs/screenshots/ (see that folder's README for how to
capture them). Once added, they are referenced here:
- JWT auth with a server-side signing key (stored in
.env/appsettings.Development.json, never committed) — now includesroleclaim (User/Demo) - Sliding sessions: short-lived access token + long-lived refresh token (rotated on each use, stored hashed in the DB)
- Google OAuth "Sign in with Google" (ID-token flow; the Google Client ID is public, no Client Secret required)
- Demo user: read-only account (
demo@bikontrol.com,Role=Demo) viaPOST /api/auth/demo+ frontend one-click demo; write operations enforced server-side (403) and hidden in UI. The demo tenant is opt-in (Demo__Enabled, defaultfalse): when off the endpoint returns404and nothing is seeded. Anonymous demo login refuses to issue tokens when the configured email belongs to a non-demo account (403); role is read from JWTroleclaim withMapInboundClaims = false. - Motorcycle images: client resizes to JPEG, server accepts only JPEG/PNG/WebP data URLs up to ~1 MB decoded (plus the 1.4M-char DTO cap).
- Email verification (opt-out via
EmailConfirmation__Required): registration emails a hashed, time-limited confirmation link and login is blocked until confirmed. Existing accounts are grandfathered by the migration; Google/demo accounts are auto-confirmed. - Account lockout (opt-out via
Lockout__Enabled): afterLockout__MaxFailedAttempts(5) failed logins the account is locked forLockout__Minutes(15) and login returns429; a successful login or password reset clears it. - Password recovery via email (SMTP configured in
.env; reset tokens are hashed and time-limited) - Password hashing with a per-user salt
- Database isolated on an internal Docker network, never on the shared network; transport encrypted with TLS (
ssl=on+ self-signed,SslMode=Require) - Automated DB backups: local
npm run db:backup/db:restore(7-copy retention) + VPSdeploy.sh backup-dbin persistentbackups/+ cron example indocs/DEPLOYMENT.md. Every dump is verified by restoring it into a throwaway database (npm run db:verify-backup), anddeployaborts if the fresh dump does not restore. - Security headers applied at the gateway (HTTPS + HSTS,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy, COOP/CORP) and a tight CSP (self + Google Identity + Font Awesome CDN, nounsafe-eval), mirrored in the frontend container; secret rotation runbook indocs/DEPLOYMENT.md - Observability: structured JSON logs (Serilog) with per-request trace ids and an
X-Request-Idon errors; optional error tracking via Sentry (Sentry__Dsn, no-op when empty);/readydoubles as the probe for an external uptime monitor
- AGENTS.md — project snapshot (stack, layout, commands, working rules)
- opencode.json — instructions, MCP servers and permissions
- .opencode/agent/ — per-area playbooks (backend, frontend, reviewer, repo-auditor)
- .opencode/skills/ — task playbooks (migrations, tests, docker, contracts, angular, security)
- .opencode/command/ — shortcuts (
/test,/migrate,/review) - docs/specs/ — architecture, auth, database detail specs
- Add Google OAuth authentication.
- Add password recovery on login.
- Bottom nav: pressing "Estadísticas" or "Perfil" redirects to login — dead links now point to home, plus a dashboard wildcard fallback so unknown dashboard URLs never land on login.
- Fix sessions expiring too frequently.
- Edit motorcycle: the km field shows the current value (from km history) and is disabled in Edit.
- Allow uploading a custom motorcycle image (stored as a resized data URL in
Motorcycle.Image). - "Add custom maintenance" redirects to login — the route existed and navigation was correct (verified by spec); likely a stale deployed bundle, plus the dashboard wildcard fallback now prevents this class of issue.
- Predefined maintenance items don't appear — the
CleanupAllButUsersmigration had truncated the seededMaintenanceTypestable; fixed with the idempotentSeedPredefinedMaintenanceTypesre-seed migration. - Create a migrator that automatically applies new tables to the production DB — already implemented: the API runs
db.Database.Migrate()on startup in any non-Development environment (Program.cs), so production applies pending migrations automatically. - Create a read-only demo user (view-only, no edits) so people can try the app. —
POST /api/auth/demo(auto-createsdemo@bikontrol.comwithRole=Demo), JWT carriesroleclaim, write endpoints return 403 for Demo, frontend shows "Probar demo" button on login + modo solo lectura banner. - Add the missing tests. — 163 backend unit tests (auth incl. lockout/email-confirmation, middleware, interval/%-remaining matrix, write-path integrity, statistics, profile, demo guards + demo-collision, role-claim mapping, image validation, motorcycle CRUD + mapping + DTO validation) + 16 integration tests (real API + PostgreSQL via Testcontainers: write paths, auth lockout, rate limiting 429, health/readiness, migration rollback, demo-forbidden and cross-user authorization) + 172 frontend Vitest tests (statistics/profile/confirm-email views, services, guards, shared refresh included).
- DB backup and security. —
npm run db:backup/db:restore(compressed, retention 7),deploy.sh backup-dbin persistentbackups/, Postgres SSL (ssl=on+ self-signed,SslMode=Require;Trust Server Certificate=true). Multi-step writes run in transactions, optimistic concurrency viaxmin, CHECK constraints on km/intervals, read-only audit inscripts/db-integrity-audit.sql(run it + a backup before every deploy with real users). - Add the statistics view. —
/dashboard/statisticsbacked by read-onlyGET /api/statistics/summary(KPIs, maintenance health, km per bike, records by type, 6-month activity; hand-rolled SVG/CSS charts). - Add the profile view. —
/dashboard/profilebacked byGET/PUT /api/users/me+POST /api/users/me/password(name editable for all; password change only for password accounts). - Triage dev-only npm advisories (full
npm auditreports high findings undersucrase/tailwindpaths; productionnpm audit --omit=devis clean). Verify: full audit +npm run test:ui+npm run build:uiafter upgrades. - Triage transitive .NET advisories in test projects (
System.Net.Http/System.Text.RegularExpressionsvia Testcontainers, SSH.NET). Verify:dotnet list package --vulnerable+npm run test:apiafter upgrades; keep AutoMapper pinned to 12.0.1 per README rationale.