Ratify the language-learning direction and expand it into Phases 15-22

SUGGESTIONS.md (new): the product rationale. Every user gets one
(English + X) pair, X in {zh, pt-PT, fr, maybe es} - bilingual UI in the
pair, type in either language, direction inferred without a detector
(both-dictionaries spellcheck, show-both gloss on collision). Langpacks
keyed by X. LLM-minimalism as a standing principle: the LLM never gates
essential functionality; grammar-lite rules and an embedded
miscollocation list are planned as code-first layers beneath the LLM
families.

MULTIUSER_PLAN.md: all OPEN decisions settled - in-app OIDC (the
parodia.dev VPS hosting plan decides it), 30-day sliding sessions,
allowlist, migration-by-script, image-store ownership fixed alongside
auth, DreamDict imported as a package reading dict.db (module rename
prereq lives in the dreamdict repo), zh stays on ECDICT until compared.

BUILD_PLAN.md: the deferred bucket becomes checkboxed Phases 15-22
(deploy plumbing / OIDC / local-user migration / client-state
namespacing / langpack extraction / DreamDict provider / pt-PT pair /
learning loop + code-first layers) with standing rules: isolation tests
in the same commit as any user-scoped endpoint, LLM-minimalism, and
bilingual aesthetic as acceptance criteria.
This commit is contained in:
prosolis
2026-07-26 22:24:04 -07:00
parent 316b6b305d
commit dae1213c68
3 changed files with 510 additions and 52 deletions
+68 -49
View File
@@ -1,11 +1,16 @@
# Petal multi-user plan
**Status:** Phase 0 (identity plumbing) landed 2026-07-26 in `6901cdb`. Everything
below it is proposed, not agreed.
**Status:** Phase 0 (identity plumbing) landed 2026-07-26 in `6901cdb`.
**All OPEN decisions ratified by the user 2026-07-26** (recommendations
accepted as written) — see each OPEN for its settled answer. Execution phases
live in `BUILD_PLAN.md` (Phase 15 onward); product rationale for the language
work is in `SUGGESTIONS.md`.
This document exists to be argued with. Sections marked **OPEN** are real
decisions with real trade-offs, not rhetorical questions — a reviewer should
push back on them. Where I have a recommendation I say so and say why.
Settled context that postdates the original draft: Petal will be hosted on the
**parodia.dev VPS**, reaching vLLM on millenia over **headscale VPN**; Piper
TTS is already installed on parodia (VPS-local, no VPN hop). The LLM is the
only cross-VPN dependency, and per the LLM-minimalism principle
(SUGGESTIONS.md §6) it must never gate essential functionality.
---
@@ -61,10 +66,12 @@ global."
## 3. Phase A — authentication
### OPEN #1: forward-auth vs. in-app OIDC
### OPEN #1: forward-auth vs. in-app OIDC — **SETTLED: Option B (in-app OIDC)**
This is the biggest fork and it should be settled first, because everything in
Phase B depends on it.
Ratified 2026-07-26. The deciding fact arrived with the deployment plan: Petal
will live on the public parodia.dev VPS, which is exactly the environment where
Option A's "must never be reachable except through Traefik" invariant is a
footgun. Original analysis kept below for the record.
**Option A — Traefik forward-auth to an Authentik outpost.**
Traefik is already in the deferred deploy bucket. Authentik's outpost terminates
@@ -109,10 +116,10 @@ Does the operator want Petal to be independently deployable?
Preferred over signed stateless cookies because it makes logout and
revocation actually work — worth the one table.
- Cookie: `HttpOnly`, `SameSite=Lax`, `Secure` when `BASE_URL` is https.
- **OPEN #2:** session lifetime. This is a personal writing tool used daily on
trusted devices; a 30-day sliding expiry is kind, an 8-hour one is
conventional. I lean long — an editor that logs you out mid-draft is hostile,
and the auto-save makes a surprise 401 genuinely costly. Needs a decision.
- **OPEN #2 — SETTLED: 30-day sliding expiry** (ratified 2026-07-26). An
editor that logs you out mid-draft is hostile, and auto-save makes a
surprise 401 genuinely costly. Sliding: each authenticated request extends
the session.
### The 401 problem
@@ -136,9 +143,11 @@ On first successful login, upsert a `users` row from the OIDC claims
(`sub``users.id`, plus email and name). No signup flow; whoever Authentik lets
in gets an account.
**OPEN #3:** should there be an allowlist? Authentik may host other applications
with a broader user set than Petal should have. A `PETAL_ALLOWED_SUBS` env var,
or an Authentik group check on a claim, would gate it. Probably yes, cheaply.
**OPEN #3 — SETTLED: yes, allowlist** (ratified 2026-07-26). Authentik may
host other applications with a broader user set than Petal should have. Gate
via a `PETAL_ALLOWED_SUBS` env var (or an Authentik group claim check —
implementer's choice, env var is simpler); a valid login not on the list gets
a warm bilingual "this Petal isn't yours to write in" page, not a 500.
### Migrating the existing `local` user
@@ -159,9 +168,9 @@ person logs in once. Sequence: deploy auth → she logs in → new empty account
created → stop app, back up, run script to move `local`'s rows onto her real id,
delete the empty row → restart.
**OPEN #4:** is that acceptable, or is a small admin endpoint preferable to a
script? The existing project convention (`scripts/`, hand-run Python) suggests a
script is in keeping.
**OPEN #4 — SETTLED: documented one-off script** (ratified 2026-07-26), run
with the app stopped and a DB backup taken first, per the existing `scripts/`
convention. No admin endpoint.
---
@@ -186,10 +195,9 @@ image uploaded by two users is stored once on disk and simply has two rows, so
deduplication survives. Deleting the last row referencing a hash removes the
file.
**OPEN #5:** is this worth doing before real multi-user, or is it acceptable to
ship auth first and treat this as a known limitation? I lean toward doing it *in
the same phase as auth*, since the moment a second account exists the exposure is
real and the fix requires a migration either way.
**OPEN #5 — SETTLED: fix it in the same phase as auth** (ratified
2026-07-26). The moment a second account exists the exposure is real, and the
fix requires a migration either way.
### Frontend `localStorage`
@@ -241,7 +249,13 @@ It also carries data Petal has no equivalent for and could use: `Antonyms`,
So Phase D stops being gated on data and becomes an integration decision.
### OPEN #6a (new): how to integrate
### OPEN #6a (new): how to integrate — **SETTLED: Option 3** (ratified 2026-07-26)
Import the package, open `dict.db` read-only. Prerequisite stands: DreamDict's
module path must be renamed (or `replace`-directed) first — **that change
lives in the dreamdict repo, not this one.** The migration caution below also
stands: pt-PT/fr wire to DreamDict first; zh stays on ECDICT until compared on
real lookups. Options kept below for the record.
**Option 1 — HTTP client.** Petal calls DreamDict on localhost:7777, exactly the
pattern already used for Piper TTS (including graceful degradation when it's
@@ -305,18 +319,26 @@ the phonetic dataset, the EN voice — is unaffected and stays shared.
## 7. Suggested sequence
1. **Settle OPEN #1** (forward-auth vs. in-app OIDC). Everything else follows.
2. Deploy plumbing: Dockerfile, Traefik, real hostname, HTTPS. Auth needs a
stable `BASE_URL` and a redirect URI regardless of which option wins.
3. Auth itself: `Resolver` implementation, sessions if applicable, frontend 401
handling.
1. ~~Settle OPEN #1~~ **Settled: in-app OIDC.**
2. Deploy plumbing: Dockerfile, Traefik, real hostname on parodia.dev, HTTPS,
headscale route to vLLM on millenia (bound to the headscale interface
only), VPS-local Piper, off-VPS DB backup. Auth needs a stable `BASE_URL`
and a redirect URI.
3. Auth itself: OIDC `Resolver`, sessions table (30-day sliding), allowlist,
frontend 401 handling with draft preservation.
4. Image store table + migration (same phase, per OPEN #5).
5. Provision the second real account; migrate `local`'s data.
6. `localStorage` namespacing.
7. Per-user language. **No longer gated on data** — DreamDict covers all four
languages. Sequence within it: rename DreamDict's module path → wire it in as
a lexicon provider → pt-PT/fr first → compare zh quality → converge if it
holds.
5. Provision the second real account; migrate `local`'s data (script, app
stopped, backup first).
6. `localStorage` namespacing (key by user **and** language — see
SUGGESTIONS.md §8).
7. Per-user language pair. **No longer gated on data** — DreamDict covers all
four languages. Sequence within it: rename DreamDict's module path → wire
it in as a lexicon provider → pt-PT/fr first → compare zh quality →
converge if it holds. The pair model and langpack shape are specified in
SUGGESTIONS.md §1–§3.
These are expanded into checkboxed execution phases in `BUILD_PLAN.md`
(Phase 15 onward) — that file remains the source of truth for progress.
---
@@ -337,19 +359,16 @@ the phonetic dataset, the EN voice — is unaffected and stays shared.
---
## 9. Questions for the reviewer
## 9. Questions for the reviewer — all answered 2026-07-26
1. Forward-auth or in-app OIDC? (OPEN #1 — the one that matters most)
2. Session lifetime, given a daily-use editor with auto-save? (OPEN #2)
3. Allowlist Petal accounts separately from Authentik's user set? (OPEN #3)
4. Migration script vs. admin endpoint for moving `local`'s data? (OPEN #4)
5. Fix the image store alongside auth, or ship auth with it as a known
limitation? (OPEN #5)
6. ~~Is there a usable open English↔European-Portuguese dictionary dataset?~~
**Answered: DreamDict**, which covers en/fr/pt-PT/zh. The live question is now
*how* to integrate it — HTTP service, build-time extraction, or importing the
package and opening `dict.db` read-only. (OPEN #6a; I recommend the third)
7. Does replacing the zh gloss (ECDICT → CC-CEDICT) risk regressing a feature in
daily use, and should zh stay on ECDICT until the two are compared?
Anything above that reads as settled but shouldn't be is also fair game.
1. ~~Forward-auth or in-app OIDC?~~ **In-app OIDC** (OPEN #1).
2. ~~Session lifetime?~~ **30-day sliding** (OPEN #2).
3. ~~Allowlist?~~ **Yes**, `PETAL_ALLOWED_SUBS` or group claim (OPEN #3).
4. ~~Script vs. admin endpoint?~~ **Script**, app stopped, backup first (OPEN #4).
5. ~~Image store timing?~~ **Same phase as auth** (OPEN #5).
6. ~~pt-PT dictionary data?~~ **DreamDict**, integrated per **Option 3**
(import package, read-only `dict.db`; module rename is the prerequisite)
(OPEN #6a).
7. ~~zh gloss regression risk?~~ **zh stays on ECDICT** until compared against
DreamDict on real lookups from her actual documents; converge only if
quality holds.