Skip to content

Commit fc8fff6

Browse files
InfantLabclaude
andcommitted
fix: auto-create weekly-rhythms tables on plugin startup
The plugin now ensures tables exist before the first scheduler sweep, preventing errors when the migration hasn't been applied yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b5483d7 commit fc8fff6

1 file changed

Lines changed: 70 additions & 1 deletion

File tree

app/server/plugins/weekly-rhythms.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,92 @@
33
*
44
* Bootstraps the scheduler sweep on server startup and sets up a periodic
55
* interval to process due generation and delivery work.
6+
* Auto-creates tables if they don't exist yet.
67
*/
78

89
import { createLogger } from "~/server/utils/logger";
10+
import { sql } from "drizzle-orm";
11+
import { db } from "~/server/db";
912

1013
const logger = createLogger("plugin:weekly-rhythms");
1114

1215
const SWEEP_INTERVAL_MS = 60 * 1000; // 1 minute
1316

17+
/** Ensure weekly-rhythms tables exist (safe for repeated calls) */
18+
async function ensureTables(): Promise<boolean> {
19+
try {
20+
const result = await db.all<{ name: string }>(
21+
sql`SELECT name FROM sqlite_master WHERE type='table' AND name='weekly_rhythm_settings'`,
22+
);
23+
if (result.length > 0) return true;
24+
25+
// Tables missing — create them
26+
logger.info("Creating weekly-rhythms tables");
27+
const ddl = [
28+
sql`CREATE TABLE IF NOT EXISTS weekly_rhythm_settings (
29+
id TEXT PRIMARY KEY, user_id TEXT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
30+
celebration_enabled INTEGER NOT NULL DEFAULT 0, encouragement_enabled INTEGER NOT NULL DEFAULT 0,
31+
celebration_tier TEXT NOT NULL DEFAULT 'stats_only', delivery_channels TEXT NOT NULL,
32+
generation_schedule TEXT NOT NULL, onboarding_completed_at TEXT, cloud_privacy_acknowledged_at TEXT,
33+
private_ai_unavailable_dismissed_at TEXT, email_unsubscribed_at TEXT, email_unsubscribe_source TEXT,
34+
consecutive_email_failures INTEGER NOT NULL DEFAULT 0, last_email_failure_at TEXT,
35+
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
36+
)`,
37+
sql`CREATE TABLE IF NOT EXISTS weekly_stats_snapshots (
38+
id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
39+
kind TEXT NOT NULL, week_start_date TEXT NOT NULL, week_end_date TEXT NOT NULL, timezone TEXT NOT NULL,
40+
period_range TEXT NOT NULL, general_progress TEXT NOT NULL, rhythm_wins TEXT NOT NULL,
41+
encouragement_context TEXT, generated_at TEXT NOT NULL
42+
)`,
43+
sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_weekly_snapshots_user_kind_week ON weekly_stats_snapshots(user_id, kind, week_start_date)`,
44+
sql`CREATE INDEX IF NOT EXISTS idx_weekly_snapshots_kind_generated ON weekly_stats_snapshots(kind, generated_at)`,
45+
sql`CREATE TABLE IF NOT EXISTS weekly_messages (
46+
id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
47+
snapshot_id TEXT NOT NULL REFERENCES weekly_stats_snapshots(id) ON DELETE CASCADE,
48+
kind TEXT NOT NULL, week_start_date TEXT NOT NULL, tier_requested TEXT NOT NULL, tier_applied TEXT NOT NULL,
49+
fallback_reason TEXT, status TEXT NOT NULL DEFAULT 'generated', title TEXT NOT NULL,
50+
summary_blocks TEXT NOT NULL, narrative_text TEXT, email_subject TEXT, email_html TEXT, email_text TEXT,
51+
in_app_visible_from TEXT NOT NULL, scheduled_delivery_at TEXT, delivered_at TEXT, dismissed_at TEXT,
52+
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
53+
)`,
54+
sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_weekly_messages_user_kind_week ON weekly_messages(user_id, kind, week_start_date)`,
55+
sql`CREATE INDEX IF NOT EXISTS idx_weekly_messages_status_scheduled ON weekly_messages(status, scheduled_delivery_at)`,
56+
sql`CREATE TABLE IF NOT EXISTS weekly_delivery_attempts (
57+
id TEXT PRIMARY KEY, message_id TEXT NOT NULL REFERENCES weekly_messages(id) ON DELETE CASCADE,
58+
channel TEXT NOT NULL, status TEXT NOT NULL, attempt_number INTEGER NOT NULL,
59+
scheduled_for TEXT NOT NULL, attempted_at TEXT, retry_after TEXT, provider TEXT,
60+
provider_message_id TEXT, failure_code TEXT, failure_message TEXT, raw_response TEXT,
61+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
62+
)`,
63+
sql`CREATE INDEX IF NOT EXISTS idx_weekly_delivery_message ON weekly_delivery_attempts(message_id)`,
64+
sql`CREATE INDEX IF NOT EXISTS idx_weekly_delivery_retry ON weekly_delivery_attempts(status, retry_after)`,
65+
];
66+
for (const stmt of ddl) {
67+
await db.run(stmt);
68+
}
69+
logger.info("Weekly-rhythms tables created");
70+
return true;
71+
} catch (err) {
72+
logger.error("Failed to ensure weekly-rhythms tables", err as Error);
73+
return false;
74+
}
75+
}
76+
1477
export default defineNitroPlugin((nitroApp) => {
1578
logger.info("Weekly rhythms plugin initializing");
1679

1780
let sweepTimer: ReturnType<typeof setInterval> | null = null;
1881

1982
// Delay first sweep to let the server fully start
2083
const startupDelay = setTimeout(async () => {
84+
// Ensure tables exist before first sweep
85+
const tablesReady = await ensureTables();
86+
if (!tablesReady) {
87+
logger.warn("Weekly rhythms tables not available, skipping scheduler");
88+
return;
89+
}
90+
2191
try {
22-
// Dynamic import to avoid circular dependency at plugin load time
2392
const { runSchedulerSweep } = await import(
2493
"~/server/services/weekly-rhythms/scheduler"
2594
);

0 commit comments

Comments
 (0)