feat: Status-Pipeline (reviewed), Audio-Verknüpfung+Coverage, EP-Fortschritt, Wort-Generierung
- reviewed-Status für objects/questions/statements/pairs (Constraints) - feed: nur fertige Inhalte (published + Bild + Audio-Gate), audio_url - pairs: Publish-Gating (draft→published = 409) - audios: source_table/source_id/source_field/language + Unique-Index; generate-for, generate-batch, GET /coverage; voices.js (Voice je Sprache) - auth: POST /auth/progress, /auth/me mit total_ep/streak/level; users_public EP-Spalten + user_pair_progress.earned_points - claude: POST /generate-words; words POST akzeptiert status Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -132,7 +132,7 @@ async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS questions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'blocked', 'published')),
|
||||
CHECK (status IN ('draft', 'reviewed', 'blocked', 'published')),
|
||||
sentence_de TEXT,
|
||||
sentence_en TEXT,
|
||||
sentence_sv TEXT,
|
||||
@@ -152,7 +152,7 @@ async function migrate() {
|
||||
for (const col of questionCols)
|
||||
await query(`ALTER TABLE questions ADD COLUMN IF NOT EXISTS ${col}`).catch(() => {});
|
||||
await query(`ALTER TABLE questions DROP CONSTRAINT IF EXISTS questions_status_check`).catch(() => {});
|
||||
await query(`ALTER TABLE questions ADD CONSTRAINT questions_status_check CHECK (status IN ('draft', 'blocked', 'published'))`).catch(() => {});
|
||||
await query(`ALTER TABLE questions ADD CONSTRAINT questions_status_check CHECK (status IN ('draft', 'reviewed', 'blocked', 'published'))`).catch(() => {});
|
||||
|
||||
await query(`
|
||||
DROP TRIGGER IF EXISTS questions_updated_at ON questions;
|
||||
@@ -165,7 +165,7 @@ async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS statements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'blocked', 'published')),
|
||||
CHECK (status IN ('draft', 'reviewed', 'blocked', 'published')),
|
||||
negative_sentence_de TEXT,
|
||||
negative_sentence_en TEXT,
|
||||
negative_sentence_sv TEXT,
|
||||
@@ -189,7 +189,7 @@ async function migrate() {
|
||||
for (const col of stmtCols)
|
||||
await query(`ALTER TABLE statements ADD COLUMN IF NOT EXISTS ${col}`).catch(() => {});
|
||||
await query(`ALTER TABLE statements DROP CONSTRAINT IF EXISTS statements_status_check`).catch(() => {});
|
||||
await query(`ALTER TABLE statements ADD CONSTRAINT statements_status_check CHECK (status IN ('draft', 'blocked', 'published'))`).catch(() => {});
|
||||
await query(`ALTER TABLE statements ADD CONSTRAINT statements_status_check CHECK (status IN ('draft', 'reviewed', 'blocked', 'published'))`).catch(() => {});
|
||||
|
||||
await query(`
|
||||
DROP TRIGGER IF EXISTS statements_updated_at ON statements;
|
||||
@@ -221,7 +221,7 @@ async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS pairs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'blocked', 'published')),
|
||||
CHECK (status IN ('draft', 'reviewed', 'blocked', 'published')),
|
||||
difficulty_level SMALLINT CHECK (difficulty_level BETWEEN 1 AND 50),
|
||||
answer_type VARCHAR(20) NOT NULL
|
||||
CHECK (answer_type IN ('yes_no', 'text', 'word')),
|
||||
@@ -253,7 +253,7 @@ async function migrate() {
|
||||
await query(`ALTER TABLE pairs ADD COLUMN IF NOT EXISTS ${col}`).catch(() => {});
|
||||
}
|
||||
await query(`ALTER TABLE pairs DROP CONSTRAINT IF EXISTS pairs_status_check`).catch(() => {});
|
||||
await query(`ALTER TABLE pairs ADD CONSTRAINT pairs_status_check CHECK (status IN ('draft', 'blocked', 'published'))`).catch(() => {});
|
||||
await query(`ALTER TABLE pairs ADD CONSTRAINT pairs_status_check CHECK (status IN ('draft', 'reviewed', 'blocked', 'published'))`).catch(() => {});
|
||||
await query(`ALTER TABLE pairs DROP CONSTRAINT IF EXISTS pairs_answer_type_check`).catch(() => {});
|
||||
await query(`ALTER TABLE pairs ADD CONSTRAINT pairs_answer_type_check CHECK (answer_type IN ('yes_no', 'text', 'word'))`).catch(() => {});
|
||||
await query(`ALTER TABLE pairs DROP CONSTRAINT IF EXISTS pairs_difficulty_level_check`).catch(() => {});
|
||||
@@ -274,7 +274,7 @@ async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
CHECK (status IN ('draft', 'blocked', 'published')),
|
||||
CHECK (status IN ('draft', 'reviewed', 'blocked', 'published')),
|
||||
selections JSONB,
|
||||
notes TEXT,
|
||||
blocked_topic TEXT,
|
||||
@@ -285,6 +285,9 @@ async function migrate() {
|
||||
)
|
||||
`);
|
||||
|
||||
await query(`ALTER TABLE objects DROP CONSTRAINT IF EXISTS objects_status_check`).catch(() => {});
|
||||
await query(`ALTER TABLE objects ADD CONSTRAINT objects_status_check CHECK (status IN ('draft', 'reviewed', 'blocked', 'published'))`).catch(() => {});
|
||||
|
||||
await query(`
|
||||
DROP TRIGGER IF EXISTS objects_updated_at ON objects;
|
||||
CREATE TRIGGER objects_updated_at
|
||||
@@ -474,6 +477,11 @@ async function migrate() {
|
||||
await query(`ALTER TABLE users_public ADD CONSTRAINT users_public_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE`).catch(() => {});
|
||||
await query(`CREATE UNIQUE INDEX IF NOT EXISTS users_public_user_id_idx ON users_public (user_id) WHERE user_id IS NOT NULL`);
|
||||
|
||||
// Gamification: EP-Total, Streak, letzter Übungstag
|
||||
await query(`ALTER TABLE users_public ADD COLUMN IF NOT EXISTS total_ep INTEGER NOT NULL DEFAULT 0`).catch(() => {});
|
||||
await query(`ALTER TABLE users_public ADD COLUMN IF NOT EXISTS streak_days INTEGER NOT NULL DEFAULT 0`).catch(() => {});
|
||||
await query(`ALTER TABLE users_public ADD COLUMN IF NOT EXISTS last_practice_at TIMESTAMPTZ`).catch(() => {});
|
||||
|
||||
// Seed languages (de exists, add en + sv)
|
||||
// Full unique constraint (not partial) so ON CONFLICT works cleanly
|
||||
await query(`CREATE UNIQUE INDEX IF NOT EXISTS languages_short_en_idx ON languages (short_en)`).catch(() => {});
|
||||
@@ -507,6 +515,8 @@ async function migrate() {
|
||||
)
|
||||
`);
|
||||
|
||||
await query(`ALTER TABLE user_pair_progress ADD COLUMN IF NOT EXISTS earned_points INTEGER NOT NULL DEFAULT 0`).catch(() => {});
|
||||
|
||||
await query(`
|
||||
CREATE OR REPLACE FUNCTION update_last_seen_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
@@ -543,6 +553,18 @@ async function migrate() {
|
||||
)
|
||||
`);
|
||||
|
||||
// Verknüpfung Audio → Quelle (Wort/Frage/Statement) + Sprache.
|
||||
// source_field: 'titel' | 'sentence' | 'positive_sentence' | 'negative_sentence'
|
||||
await query(`ALTER TABLE audios ADD COLUMN IF NOT EXISTS source_table TEXT`).catch(() => {});
|
||||
await query(`ALTER TABLE audios ADD COLUMN IF NOT EXISTS source_id UUID`).catch(() => {});
|
||||
await query(`ALTER TABLE audios ADD COLUMN IF NOT EXISTS source_field TEXT`).catch(() => {});
|
||||
await query(`ALTER TABLE audios ADD COLUMN IF NOT EXISTS language VARCHAR(10)`).catch(() => {});
|
||||
await query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS audios_source_uq
|
||||
ON audios (source_table, source_id, source_field, language)
|
||||
WHERE source_table IS NOT NULL
|
||||
`).catch(() => {});
|
||||
|
||||
await query(`
|
||||
DROP TRIGGER IF EXISTS audios_updated_at ON audios;
|
||||
CREATE TRIGGER audios_updated_at
|
||||
|
||||
Reference in New Issue
Block a user