formatting

This commit is contained in:
lila 2026-04-17 15:52:50 +02:00
parent 7f56ad89e6
commit a6d8ddec3b
5 changed files with 82 additions and 221 deletions

View file

@ -12,19 +12,19 @@ This document describes the production deployment of the lila vocabulary trainer
### Subdomain Routing ### Subdomain Routing
| Subdomain | Service | Container port | | Subdomain | Service | Container port |
|---|---|---| | ------------------- | ------------------------------------- | -------------- |
| `lilastudy.com` | Frontend (nginx serving static files) | 80 | | `lilastudy.com` | Frontend (nginx serving static files) | 80 |
| `api.lilastudy.com` | Express API | 3000 | | `api.lilastudy.com` | Express API | 3000 |
| `git.lilastudy.com` | Forgejo (web UI + container registry) | 3000 | | `git.lilastudy.com` | Forgejo (web UI + container registry) | 3000 |
### Ports Exposed to the Internet ### Ports Exposed to the Internet
| Port | Service | | Port | Service |
|---|---| | ---- | -------------------------------- |
| 80 | Caddy (HTTP, redirects to HTTPS) | | 80 | Caddy (HTTP, redirects to HTTPS) |
| 443 | Caddy (HTTPS) | | 443 | Caddy (HTTPS) |
| 2222 | Forgejo SSH (git clone/push) | | 2222 | Forgejo SSH (git clone/push) |
All other services (Postgres, API, frontend) communicate only over the internal Docker network. All other services (Postgres, API, frontend) communicate only over the internal Docker network.

View file

@ -290,15 +290,15 @@ After completing a task: share the code, ask what to refactor and why. The LLM s
## 11. Post-MVP Ladder ## 11. Post-MVP Ladder
| Phase | What it adds | Status | | Phase | What it adds | Status |
| ----------------- | ------------------------------------------------------------------------------- | ------ | | ------------------- | ----------------------------------------------------------------------- | ------ |
| Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ | | Auth | Better Auth (Google + GitHub), embedded in Express API, user rows in DB | ✅ |
| Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ | | Deployment | Docker Compose, Caddy, Forgejo, CI/CD, Hetzner VPS | ✅ |
| Hardening (partial) | CI/CD pipeline, DB backups | ✅ | | Hardening (partial) | CI/CD pipeline, DB backups | ✅ |
| User Stats | Games played, score history, profile page | ❌ | | User Stats | Games played, score history, profile page | ❌ |
| Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ | | Multiplayer Lobby | Room creation, join by code, WebSocket connection | ❌ |
| Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ | | Multiplayer Game | Simultaneous answers, server timer, live scores, winner screen | ❌ |
| Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ | | Hardening (rest) | Rate limiting, error boundaries, monitoring, accessibility | ❌ |
### Future Data Model Extensions (deferred, additive) ### Future Data Model Extensions (deferred, additive)

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -368,12 +350,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -383,9 +361,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -435,12 +411,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -450,10 +422,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -488,12 +457,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -501,12 +466,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -514,10 +475,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -591,10 +549,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -650,9 +605,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -748,12 +701,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -763,11 +712,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -844,9 +789,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -927,9 +870,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -110,12 +110,8 @@
"name": "account_user_id_user_id_fk", "name": "account_user_id_user_id_fk",
"tableFrom": "account", "tableFrom": "account",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -149,12 +145,8 @@
"name": "deck_terms_deck_id_decks_id_fk", "name": "deck_terms_deck_id_decks_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -162,12 +154,8 @@
"name": "deck_terms_term_id_terms_id_fk", "name": "deck_terms_term_id_terms_id_fk",
"tableFrom": "deck_terms", "tableFrom": "deck_terms",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -175,10 +163,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_terms_deck_id_term_id_pk": { "deck_terms_deck_id_term_id_pk": {
"name": "deck_terms_deck_id_term_id_pk", "name": "deck_terms_deck_id_term_id_pk",
"columns": [ "columns": ["deck_id", "term_id"]
"deck_id",
"term_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -265,10 +250,7 @@
"unique_deck_name": { "unique_deck_name": {
"name": "unique_deck_name", "name": "unique_deck_name",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["name", "source_language"]
"name",
"source_language"
]
} }
}, },
"policies": {}, "policies": {},
@ -335,12 +317,8 @@
"name": "lobbies_host_user_id_user_id_fk", "name": "lobbies_host_user_id_user_id_fk",
"tableFrom": "lobbies", "tableFrom": "lobbies",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["host_user_id"],
"host_user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -350,9 +328,7 @@
"lobbies_code_unique": { "lobbies_code_unique": {
"name": "lobbies_code_unique", "name": "lobbies_code_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["code"]
"code"
]
} }
}, },
"policies": {}, "policies": {},
@ -401,12 +377,8 @@
"name": "lobby_players_lobby_id_lobbies_id_fk", "name": "lobby_players_lobby_id_lobbies_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "lobbies", "tableTo": "lobbies",
"columnsFrom": [ "columnsFrom": ["lobby_id"],
"lobby_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -414,12 +386,8 @@
"name": "lobby_players_user_id_user_id_fk", "name": "lobby_players_user_id_user_id_fk",
"tableFrom": "lobby_players", "tableFrom": "lobby_players",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -427,10 +395,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"lobby_players_lobby_id_user_id_pk": { "lobby_players_lobby_id_user_id_pk": {
"name": "lobby_players_lobby_id_user_id_pk", "name": "lobby_players_lobby_id_user_id_pk",
"columns": [ "columns": ["lobby_id", "user_id"]
"lobby_id",
"user_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -514,12 +479,8 @@
"name": "session_user_id_user_id_fk", "name": "session_user_id_user_id_fk",
"tableFrom": "session", "tableFrom": "session",
"tableTo": "user", "tableTo": "user",
"columnsFrom": [ "columnsFrom": ["user_id"],
"user_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -529,9 +490,7 @@
"session_token_unique": { "session_token_unique": {
"name": "session_token_unique", "name": "session_token_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["token"]
"token"
]
} }
}, },
"policies": {}, "policies": {},
@ -581,12 +540,8 @@
"name": "term_glosses_term_id_terms_id_fk", "name": "term_glosses_term_id_terms_id_fk",
"tableFrom": "term_glosses", "tableFrom": "term_glosses",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -596,10 +551,7 @@
"unique_term_gloss": { "unique_term_gloss": {
"name": "unique_term_gloss", "name": "unique_term_gloss",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code"]
"term_id",
"language_code"
]
} }
}, },
"policies": {}, "policies": {},
@ -634,12 +586,8 @@
"name": "term_topics_term_id_terms_id_fk", "name": "term_topics_term_id_terms_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@ -647,12 +595,8 @@
"name": "term_topics_topic_id_topics_id_fk", "name": "term_topics_topic_id_topics_id_fk",
"tableFrom": "term_topics", "tableFrom": "term_topics",
"tableTo": "topics", "tableTo": "topics",
"columnsFrom": [ "columnsFrom": ["topic_id"],
"topic_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -660,10 +604,7 @@
"compositePrimaryKeys": { "compositePrimaryKeys": {
"term_topics_term_id_topic_id_pk": { "term_topics_term_id_topic_id_pk": {
"name": "term_topics_term_id_topic_id_pk", "name": "term_topics_term_id_topic_id_pk",
"columns": [ "columns": ["term_id", "topic_id"]
"term_id",
"topic_id"
]
} }
}, },
"uniqueConstraints": {}, "uniqueConstraints": {},
@ -737,10 +678,7 @@
"unique_source_id": { "unique_source_id": {
"name": "unique_source_id", "name": "unique_source_id",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["source", "source_id"]
"source",
"source_id"
]
} }
}, },
"policies": {}, "policies": {},
@ -796,9 +734,7 @@
"topics_slug_unique": { "topics_slug_unique": {
"name": "topics_slug_unique", "name": "topics_slug_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["slug"]
"slug"
]
} }
}, },
"policies": {}, "policies": {},
@ -894,12 +830,8 @@
"name": "translations_term_id_terms_id_fk", "name": "translations_term_id_terms_id_fk",
"tableFrom": "translations", "tableFrom": "translations",
"tableTo": "terms", "tableTo": "terms",
"columnsFrom": [ "columnsFrom": ["term_id"],
"term_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@ -909,11 +841,7 @@
"unique_translations": { "unique_translations": {
"name": "unique_translations", "name": "unique_translations",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["term_id", "language_code", "text"]
"term_id",
"language_code",
"text"
]
} }
}, },
"policies": {}, "policies": {},
@ -990,9 +918,7 @@
"user_email_unique": { "user_email_unique": {
"name": "user_email_unique", "name": "user_email_unique",
"nullsNotDistinct": false, "nullsNotDistinct": false,
"columns": [ "columns": ["email"]
"email"
]
} }
}, },
"policies": {}, "policies": {},
@ -1073,9 +999,5 @@
"roles": {}, "roles": {},
"policies": {}, "policies": {},
"views": {}, "views": {},
"_meta": { "_meta": { "columns": {}, "schemas": {}, "tables": {} }
"columns": {}, }
"schemas": {},
"tables": {}
}
}

View file

@ -52,4 +52,4 @@
"breakpoints": true "breakpoints": true
} }
] ]
} }