From 3b536006f117ce97129c1ebc8b8574cd950f910c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:36:48 +0000 Subject: [PATCH 1/2] Changes before error encountered Agent-Logs-Url: https://github.com/JacinthaJanice/PrepPath/sessions/a5c56b8f-687f-4fcf-b153-e8e3852462ed Co-authored-by: JacinthaJanice <132421493+JacinthaJanice@users.noreply.github.com> --- backend/scripts/test-api.js | 163 +++++++++++++++++++++--- backend/server.js | 2 +- database/supabase-schema.sql | 237 +++++++++++++++++------------------ frontend/api.js | 2 +- frontend/config.js | 21 ++-- frontend/realtime-sync.js | 2 +- 6 files changed, 277 insertions(+), 150 deletions(-) diff --git a/backend/scripts/test-api.js b/backend/scripts/test-api.js index e6a3cd1..faf82df 100644 --- a/backend/scripts/test-api.js +++ b/backend/scripts/test-api.js @@ -6,6 +6,7 @@ const BASE = `http://localhost:${process.env.PORT || 3001}`; const USER = 'user_main'; let passed = 0, failed = 0; +let createdTaskId = null; async function test(name, fn) { try { @@ -26,74 +27,202 @@ async function req(method, path, body) { }); const data = await res.json(); if (!data.success && res.status !== 200) throw new Error(data.error || 'Failed'); - return data; + return { data, status: res.status }; +} + +async function reqRaw(method, path, body) { + const res = await fetch(`${BASE}${path}`, { + method, + headers: { 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined + }); + return { status: res.status, data: await res.json() }; } async function run() { console.log(`\n๐Ÿงช Testing PrepPath API at ${BASE}\n`); - await test('Health check', async () => { + // โ”€โ”€ Health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('Health'); + await test('Health check returns ok', async () => { const r = await fetch(`${BASE}/health`); const d = await r.json(); if (d.status !== 'ok') throw new Error('Health check failed'); + if (!d.timestamp) throw new Error('Missing timestamp'); }); + // โ”€โ”€ Progress โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('\nProgress'); await test('GET progress (empty ok)', async () => { await req('GET', `/api/progress/${USER}`); }); await test('POST progress (save state)', async () => { - await req('POST', `/api/progress/${USER}`, { + const { data } = await req('POST', `/api/progress/${USER}`, { state: { chk_t0: true, chk_t1: false, rm_rm_1_1_s0: true } }); + if (!data.data) throw new Error('No data returned after save'); }); - await test('GET progress (verify saved)', async () => { - const d = await req('GET', `/api/progress/${USER}`); - if (!d.data) throw new Error('No data returned'); + await test('GET progress (verify saved state)', async () => { + const { data } = await req('GET', `/api/progress/${USER}`); + if (!data.data) throw new Error('No data returned'); + if (data.data.state_data?.chk_t0 !== true) throw new Error('Saved state not persisted'); }); + await test('POST progress (overwrite state)', async () => { + const { data } = await req('POST', `/api/progress/${USER}`, { + state: { chk_t0: false, rm_rm_1_1_s0: true, rm_rm_1_1_s1: true } + }); + if (!data.data) throw new Error('No data returned after overwrite'); + }); + + // โ”€โ”€ Custom Tasks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('\nCustom Tasks'); await test('POST custom task', async () => { - await req('POST', `/api/tasks/${USER}`, { + const { data } = await req('POST', `/api/tasks/${USER}`, { project_id: 'proj_personal', label: 'Test task from API', difficulty: 'easy' }); + if (!data.data?.id) throw new Error('No task id returned'); + createdTaskId = data.data.id; + }); + + await test('POST custom task โ€” medium difficulty', async () => { + await req('POST', `/api/tasks/${USER}`, { + project_id: 'proj_recruiter', + label: 'Medium task', + difficulty: 'medium' + }); + }); + + await test('POST custom task โ€” missing label returns 400', async () => { + const { status, data } = await reqRaw('POST', `/api/tasks/${USER}`, { + project_id: 'proj_personal' + }); + if (status !== 400) throw new Error(`Expected 400, got ${status}`); + if (data.success !== false) throw new Error('Expected success:false'); + }); + + await test('POST custom task โ€” missing project_id returns 400', async () => { + const { status, data } = await reqRaw('POST', `/api/tasks/${USER}`, { + label: 'No project task' + }); + if (status !== 400) throw new Error(`Expected 400, got ${status}`); + if (data.success !== false) throw new Error('Expected success:false'); }); await test('GET custom tasks', async () => { - const d = await req('GET', `/api/tasks/${USER}`); - if (!Array.isArray(d.data)) throw new Error('Expected array'); + const { data } = await req('GET', `/api/tasks/${USER}`); + if (!Array.isArray(data.data)) throw new Error('Expected array'); + if (data.data.length < 1) throw new Error('Expected at least one task'); + }); + + await test('PATCH custom task โ€” toggle done', async () => { + if (!createdTaskId) throw new Error('No task id from earlier test'); + const { data } = await req('PATCH', `/api/tasks/${USER}/${createdTaskId}`, { done: true }); + if (data.data?.done !== true) throw new Error('Task not marked done'); + }); + + await test('PATCH custom task โ€” toggle not done', async () => { + if (!createdTaskId) throw new Error('No task id from earlier test'); + const { data } = await req('PATCH', `/api/tasks/${USER}/${createdTaskId}`, { done: false }); + if (data.data?.done !== false) throw new Error('Task not marked undone'); }); + await test('PATCH custom task โ€” update label', async () => { + if (!createdTaskId) throw new Error('No task id from earlier test'); + const { data } = await req('PATCH', `/api/tasks/${USER}/${createdTaskId}`, { label: 'Updated label' }); + if (data.data?.label !== 'Updated label') throw new Error('Label not updated'); + }); + + await test('DELETE custom task', async () => { + if (!createdTaskId) throw new Error('No task id from earlier test'); + await req('DELETE', `/api/tasks/${USER}/${createdTaskId}`); + }); + + await test('GET custom tasks after delete (count decreased)', async () => { + const { data } = await req('GET', `/api/tasks/${USER}`); + if (!Array.isArray(data.data)) throw new Error('Expected array'); + const deleted = data.data.find(t => t.id === createdTaskId); + if (deleted) throw new Error('Deleted task still present'); + }); + + // โ”€โ”€ Journal โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('\nJournal'); + const today = new Date().toISOString().split('T')[0]; + await test('POST journal entry', async () => { - await req('POST', `/api/journal/${USER}`, { - entry_date: new Date().toISOString().split('T')[0], + const { data } = await req('POST', `/api/journal/${USER}`, { + entry_date: today, learnt: 'C# OOP, arrays in DSA, GCP compute basics', confused: 'Inheritance vs composition', gratitude: 'Had energy to study today', verse_reflection: 'Philippians 4:13 gave me confidence' }); + if (!data.data) throw new Error('No data returned'); + }); + + await test('POST journal entry (upsert same date)', async () => { + const { data } = await req('POST', `/api/journal/${USER}`, { + entry_date: today, + learnt: 'Updated learning note', + confused: '', + gratitude: 'Grateful for progress', + verse_reflection: '' + }); + if (data.data?.learnt !== 'Updated learning note') throw new Error('Upsert did not update learnt'); }); await test('GET journal entries', async () => { - const d = await req('GET', `/api/journal/${USER}`); - if (!Array.isArray(d.data)) throw new Error('Expected array'); + const { data } = await req('GET', `/api/journal/${USER}`); + if (!Array.isArray(data.data)) throw new Error('Expected array'); + if (data.data.length < 1) throw new Error('Expected at least one entry'); + const entry = data.data.find(e => e.entry_date === today); + if (!entry) throw new Error('Today\'s entry not found'); }); + // โ”€โ”€ Stats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('\nStats'); await test('GET stats', async () => { - const d = await req('GET', `/api/stats/${USER}`); - if (!d.stats) throw new Error('No stats returned'); - console.log(` Days until Sept: ${d.stats.days_until_sept}`); + const { data } = await req('GET', `/api/stats/${USER}`); + if (!data.stats) throw new Error('No stats returned'); + const expected = ['schedule_tasks_done','roadmap_steps_done','future_steps_done', + 'project_tasks_done','custom_tasks_total','custom_tasks_done', + 'journal_entries','last_active','days_until_sept']; + for (const key of expected) { + if (!(key in data.stats)) throw new Error(`Missing stats key: ${key}`); + } + }); + + await test('GET stats โ€” days_until_sept is non-negative', async () => { + const { data } = await req('GET', `/api/stats/${USER}`); + if (data.stats.days_until_sept < 0) throw new Error('days_until_sept is negative'); + }); + + await test('GET stats โ€” journal_entries > 0 after saving', async () => { + const { data } = await req('GET', `/api/stats/${USER}`); + if (data.stats.journal_entries < 1) throw new Error('Expected at least 1 journal entry in stats'); }); - await test('404 route', async () => { + // โ”€โ”€ Error Handling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + console.log('\nError handling'); + await test('404 route returns 404', async () => { const r = await fetch(`${BASE}/api/nonexistent`); if (r.status !== 404) throw new Error('Expected 404'); }); + await test('404 returns success:false', async () => { + const r = await fetch(`${BASE}/api/nonexistent`); + const d = await r.json(); + if (d.success !== false) throw new Error('Expected success:false on 404'); + }); + + // โ”€โ”€ Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`); if (failed > 0) process.exit(1); } run().catch(console.error); + diff --git a/backend/server.js b/backend/server.js index e842411..426303e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -289,7 +289,7 @@ app.get('/api/stats/:userId', async (req, res) => { custom_tasks_done: tasks.filter(t => t.done).length, journal_entries: journal.length, last_active: progressRes.data?.updated_at || null, - days_until_sept: Math.max(0, Math.floor((new Date('2025-09-30') - new Date()) / 86400000)) + days_until_sept: Math.max(0, Math.floor((new Date(new Date().getFullYear() + (new Date().getMonth() >= 8 ? 1 : 0), 8, 30) - new Date()) / 86400000)) } }); } catch (err) { diff --git a/database/supabase-schema.sql b/database/supabase-schema.sql index b8b61a0..64094c5 100644 --- a/database/supabase-schema.sql +++ b/database/supabase-schema.sql @@ -1,171 +1,162 @@ -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- PREPPATH - SUPABASE DATABASE SCHEMA --- Run this SQL in your Supabase project โ†’ SQL Editor +-- This file mirrors database/setup.sql for reference. +-- Run database/setup.sql in your Supabase project โ†’ SQL Editor -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- 1. Create the progress/state table --- Stores user's overall progress and app state +-- Stores user's overall progress and app state as a JSONB blob CREATE TABLE IF NOT EXISTS progress ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, state_data JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now(), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(user_id) ); --- 2. Create the tasks table --- Stores user's custom tasks across projects -CREATE TABLE IF NOT EXISTS tasks ( +-- 2. Create the custom_tasks table +-- Stores user-added tasks inside each project card +CREATE TABLE IF NOT EXISTS custom_tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - project_id TEXT, + project_id TEXT NOT NULL, label TEXT NOT NULL, - difficulty TEXT DEFAULT 'easy', - done BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now() + difficulty TEXT NOT NULL DEFAULT 'easy' CHECK (difficulty IN ('easy', 'medium', 'hard')), + done BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() ); -- 3. Create the journal table --- Stores user's journal entries with mood and reflections +-- Stores evening reflection entries โ€” one row per user per day CREATE TABLE IF NOT EXISTS journal ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - title TEXT, - content TEXT, - mood TEXT, - created_at TIMESTAMP DEFAULT now(), - updated_at TIMESTAMP DEFAULT now() + entry_date DATE NOT NULL, + learnt TEXT, + confused TEXT, + gratitude TEXT, + verse_reflection TEXT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(user_id, entry_date) ); +-- 4. Study sessions table (optional, for future time-tracking feature) +CREATE TABLE IF NOT EXISTS study_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + topic TEXT NOT NULL, + duration_min INT NOT NULL, + session_date DATE NOT NULL DEFAULT CURRENT_DATE, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT now() +); + +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +-- AUTO-UPDATE TIMESTAMPS +-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_progress_updated ON progress; +CREATE TRIGGER trg_progress_updated + BEFORE UPDATE ON progress + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +DROP TRIGGER IF EXISTS trg_custom_tasks_updated ON custom_tasks; +CREATE TRIGGER trg_custom_tasks_updated + BEFORE UPDATE ON custom_tasks + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +DROP TRIGGER IF EXISTS trg_journal_updated ON journal; +CREATE TRIGGER trg_journal_updated + BEFORE UPDATE ON journal + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- ROW LEVEL SECURITY (RLS) - Users can only see their own data -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- Enable RLS on all tables ALTER TABLE progress ENABLE ROW LEVEL SECURITY; -ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; +ALTER TABLE custom_tasks ENABLE ROW LEVEL SECURITY; ALTER TABLE journal ENABLE ROW LEVEL SECURITY; +ALTER TABLE study_sessions ENABLE ROW LEVEL SECURITY; -- Progress policies -CREATE POLICY progress_select_policy ON progress - FOR SELECT USING (auth.uid() = user_id); - -CREATE POLICY progress_insert_policy ON progress - FOR INSERT WITH CHECK (auth.uid() = user_id); - -CREATE POLICY progress_update_policy ON progress - FOR UPDATE USING (auth.uid() = user_id); - -CREATE POLICY progress_delete_policy ON progress - FOR DELETE USING (auth.uid() = user_id); +CREATE POLICY progress_select ON progress FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY progress_insert ON progress FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY progress_update ON progress FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY progress_delete ON progress FOR DELETE USING (auth.uid() = user_id); --- Tasks policies -CREATE POLICY tasks_select_policy ON tasks - FOR SELECT USING (auth.uid() = user_id); - -CREATE POLICY tasks_insert_policy ON tasks - FOR INSERT WITH CHECK (auth.uid() = user_id); - -CREATE POLICY tasks_update_policy ON tasks - FOR UPDATE USING (auth.uid() = user_id); - -CREATE POLICY tasks_delete_policy ON tasks - FOR DELETE USING (auth.uid() = user_id); +-- Custom tasks policies +CREATE POLICY custom_tasks_select ON custom_tasks FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY custom_tasks_insert ON custom_tasks FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY custom_tasks_update ON custom_tasks FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY custom_tasks_delete ON custom_tasks FOR DELETE USING (auth.uid() = user_id); -- Journal policies -CREATE POLICY journal_select_policy ON journal - FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY journal_select ON journal FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY journal_insert ON journal FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY journal_update ON journal FOR UPDATE USING (auth.uid() = user_id); +CREATE POLICY journal_delete ON journal FOR DELETE USING (auth.uid() = user_id); -CREATE POLICY journal_insert_policy ON journal - FOR INSERT WITH CHECK (auth.uid() = user_id); - -CREATE POLICY journal_update_policy ON journal - FOR UPDATE USING (auth.uid() = user_id); - -CREATE POLICY journal_delete_policy ON journal - FOR DELETE USING (auth.uid() = user_id); +-- Study sessions policies +CREATE POLICY sessions_select ON study_sessions FOR SELECT USING (auth.uid() = user_id); +CREATE POLICY sessions_insert ON study_sessions FOR INSERT WITH CHECK (auth.uid() = user_id); +CREATE POLICY sessions_delete ON study_sessions FOR DELETE USING (auth.uid() = user_id); -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- ENABLE REALTIME (for live subscriptions) +-- INDEXES FOR PERFORMANCE -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- In Supabase Dashboard, go to: --- 1. Settings โ†’ Realtime โ†’ Database --- 2. Toggle ON for: public.progress, public.tasks, public.journal --- 3. Apply changes - --- Alternatively, use this SQL to enable realtime: -BEGIN; - -- For Supabase it's done via Dashboard, but we can manage publications if needed - DROP PUBLICATION IF EXISTS supabase_realtime; - CREATE PUBLICATION supabase_realtime; - ALTER PUBLICATION supabase_realtime ADD TABLE progress; - ALTER PUBLICATION supabase_realtime ADD TABLE tasks; - ALTER PUBLICATION supabase_realtime ADD TABLE journal; -COMMIT; +CREATE INDEX IF NOT EXISTS idx_progress_user_id ON progress(user_id); +CREATE INDEX IF NOT EXISTS idx_custom_tasks_user_id ON custom_tasks(user_id); +CREATE INDEX IF NOT EXISTS idx_custom_tasks_project ON custom_tasks(project_id); +CREATE INDEX IF NOT EXISTS idx_custom_tasks_done ON custom_tasks(done); +CREATE INDEX IF NOT EXISTS idx_custom_tasks_created ON custom_tasks(created_at ASC); +CREATE INDEX IF NOT EXISTS idx_journal_user_date ON journal(user_id, entry_date DESC); +CREATE INDEX IF NOT EXISTS idx_sessions_user_date ON study_sessions(user_id, session_date DESC); +CREATE INDEX IF NOT EXISTS idx_sessions_topic ON study_sessions(topic); -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- BONUS: Indexes for performance +-- ENABLE REALTIME SUBSCRIPTIONS +-- In Supabase Dashboard โ†’ Settings โ†’ Realtime โ†’ Database +-- Toggle ON for: progress, custom_tasks, journal +-- Or run the SQL below: -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -CREATE INDEX IF NOT EXISTS idx_progress_user_id ON progress(user_id); -CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id); -CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id); -CREATE INDEX IF NOT EXISTS idx_tasks_done ON tasks(done); -CREATE INDEX IF NOT EXISTS idx_journal_user_id ON journal(user_id); -CREATE INDEX IF NOT EXISTS idx_journal_created_at ON journal(created_at DESC); +-- ALTER PUBLICATION supabase_realtime ADD TABLE progress; +-- ALTER PUBLICATION supabase_realtime ADD TABLE custom_tasks; +-- ALTER PUBLICATION supabase_realtime ADD TABLE journal; -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- TESTING: Insert some sample data +-- USEFUL VIEWS -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- To test, first create a test user via Supabase Auth --- Then uncomment and update the UUID below: - -/* -INSERT INTO progress (user_id, state_data) VALUES - ('YOUR-USER-ID-HERE', '{"ringPct": 45, "tracks": {"NET": 5, "DSA": 3}}') -ON CONFLICT (user_id) DO UPDATE SET state_data = excluded.state_data; - -INSERT INTO tasks (user_id, project_id, label, difficulty, done) VALUES - ('YOUR-USER-ID-HERE', 'project_1', 'Learn TypeScript basics', 'easy', false), - ('YOUR-USER-ID-HERE', 'project_1', 'Build a REST API', 'medium', false), - ('YOUR-USER-ID-HERE', 'project_2', 'Study React hooks', 'medium', true); - -INSERT INTO journal (user_id, title, content, mood) VALUES - ('YOUR-USER-ID-HERE', 'Progress Update', 'Had a productive day learning React', 'positive'); -*/ - --- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- QUICK START CHECKLIST --- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +-- Daily completion summary +CREATE OR REPLACE VIEW daily_summary AS +SELECT + user_id, + entry_date, + learnt IS NOT NULL AND learnt != '' AS has_learnt, + gratitude IS NOT NULL AND gratitude != '' AS has_gratitude, + verse_reflection IS NOT NULL AND verse_reflection != '' AS has_verse +FROM journal; + +-- Study streak (how many consecutive days journalled per user) +CREATE OR REPLACE VIEW journal_streak AS +SELECT + user_id, + COUNT(DISTINCT entry_date) AS total_days, + MAX(entry_date) AS last_entry +FROM journal +GROUP BY user_id; -/* -โœ… Steps to complete real-time sync setup in Supabase: - -1. [] Go to your Supabase project dashboard -2. [] Click SQL Editor โ†’ New Query -3. [] Copy all the SQL above and run it (this creates tables, RLS, indexes) -4. [] Go to Settings โ†’ Realtime โ†’ Database -5. [] Toggle ON for: progress, tasks, journal tables -6. [] Click "Apply changes" (may take 30 seconds) - -7. [] Update frontend/config.js: - - SUPABASE_URL: Get from Settings โ†’ API โ†’ Project URL - - SUPABASE_ANON_KEY: Get from Settings โ†’ API โ†’ anon public key - -8. [] Deploy frontend (or run locally) -9. [] Test: - - Go to https://yourapp.com - - Click "Login for Sync" - - Enter your email and open the magic link (in your email) - - You're now logged in across devices! - -10. [] Open the app on 2 devices (desktop + mobile, or 2 browsers) -11. [] Add a task on Device A โ†’ See it appear on Device B in real-time โœจ -12. [] Toggle a task done on Device B โ†’ See the change on Device A instantly! - -That's it! Cross-device real-time sync is now active. -*/ diff --git a/frontend/api.js b/frontend/api.js index cd18c20..624ca9c 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -210,7 +210,7 @@ const API = (() => { custom_tasks_done: tasks.filter(t => t.done).length, journal_entries: journal.length, last_active: pRes.data?.updated_at || null, - days_until_sept: Math.max(0, Math.floor((new Date('2025-09-30') - new Date()) / 86400000)) + days_until_sept: Math.max(0, Math.floor((new Date(new Date().getFullYear() + (new Date().getMonth() >= 8 ? 1 : 0), 8, 30) - new Date()) / 86400000)) }; } catch {} } diff --git a/frontend/config.js b/frontend/config.js index cbc75b1..c85e8ae 100644 --- a/frontend/config.js +++ b/frontend/config.js @@ -1,14 +1,21 @@ // config.js โ€” loaded before the main app -// Change BACKEND_URL to your deployed backend once hosted +// +// Cross-device sync works via Supabase directly โ€” no backend server required. +// Set SUPABASE_URL and SUPABASE_ANON_KEY below (from Supabase โ†’ Settings โ†’ API) +// and the app will sync in real-time across all devices automatically. +// +// BACKEND_URL is only needed if you run the optional Express backend. +// Leave it empty ('') to use Supabase-direct mode (recommended for most users). +// If you deploy the backend, set it to your hosted URL: +// Render.com: 'https://preppath-api.onrender.com' +// Railway: 'https://preppath-api.up.railway.app' window.PP_CONFIG = { - // โ”€โ”€ Change this to your backend URL when deployed โ”€โ”€ - // Local dev: 'http://localhost:3001' - // Render.com: 'https://preppath-api.onrender.com' - // Railway: 'https://preppath-api.up.railway.app' - BACKEND_URL: 'http://localhost:3001', + // โ”€โ”€ Optional: only set if you run the Express backend โ”€โ”€ + // Leave empty to use Supabase directly (works on any device, no localhost needed) + BACKEND_URL: '', - // Supabase direct (fallback if no backend) + // Supabase credentials โ€” required for cross-device sync // Fill these from Supabase โ†’ Settings โ†’ API SUPABASE_URL: 'https://ydmmcxemzxxifpvdwgsw.supabase.co', SUPABASE_ANON_KEY: 'sb_publishable_5XFKr2QsT_4YFO9Wr3y1qQ_r2nQlICveyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkbW1jeGVtenh4aWZwdmR3Z3N3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU0OTM3MTIsImV4cCI6MjA5MTA2OTcxMn0.U0paO2Q7cQL-ldeuAK-55hCkrbtFyl_h-dM6u8Hz4lY', diff --git a/frontend/realtime-sync.js b/frontend/realtime-sync.js index eb3ad81..d1f9b21 100644 --- a/frontend/realtime-sync.js +++ b/frontend/realtime-sync.js @@ -181,7 +181,7 @@ const SYNC = (() => { { event: '*', schema: 'public', - table: 'tasks', + table: 'custom_tasks', filter: `user_id=eq.${userId}` }, (payload) => { From 25ce32131f6c353c0a8d5d5afc7e7f5bd02e9d9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:59:31 +0000 Subject: [PATCH 2/2] fix: realtime sync table name, schema correctness, localhost default, date helper, test coverage Agent-Logs-Url: https://github.com/JacinthaJanice/PrepPath/sessions/886d9577-21c0-475b-b94c-2dd31ff9035c Co-authored-by: JacinthaJanice <132421493+JacinthaJanice@users.noreply.github.com> --- backend/server.js | 9 ++++++++- database/supabase-schema.sql | 12 ++++++++---- frontend/api.js | 8 +++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/backend/server.js b/backend/server.js index 426303e..a10cd39 100644 --- a/backend/server.js +++ b/backend/server.js @@ -278,6 +278,13 @@ app.get('/api/stats/:userId', async (req, res) => { const futureKeys = Object.keys(state).filter(k => k.startsWith('fs_') && state[k]); const projKeys = Object.keys(state).filter(k => k.startsWith('pt_') && state[k]); + // Days remaining until 30 September of the current academic year. + // If September has already passed this calendar year, target next year's date. + const now = new Date(); + const targetYear = now.getMonth() >= 8 ? now.getFullYear() + 1 : now.getFullYear(); + const septTarget = new Date(targetYear, 8, 30); // month 8 = September (0-indexed) + const daysUntilSept = Math.max(0, Math.floor((septTarget - now) / 86400000)); + res.json({ success: true, stats: { @@ -289,7 +296,7 @@ app.get('/api/stats/:userId', async (req, res) => { custom_tasks_done: tasks.filter(t => t.done).length, journal_entries: journal.length, last_active: progressRes.data?.updated_at || null, - days_until_sept: Math.max(0, Math.floor((new Date(new Date().getFullYear() + (new Date().getMonth() >= 8 ? 1 : 0), 8, 30) - new Date()) / 86400000)) + days_until_sept: daysUntilSept } }); } catch (err) { diff --git a/database/supabase-schema.sql b/database/supabase-schema.sql index 64094c5..5617aea 100644 --- a/database/supabase-schema.sql +++ b/database/supabase-schema.sql @@ -1,7 +1,7 @@ -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- PREPPATH - SUPABASE DATABASE SCHEMA --- This file mirrors database/setup.sql for reference. --- Run database/setup.sql in your Supabase project โ†’ SQL Editor +-- This is the canonical schema file. Run this SQL in your Supabase project +-- via: Supabase Dashboard โ†’ SQL Editor โ†’ New Query โ†’ paste โ†’ Run -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -- 1. Create the progress/state table @@ -58,6 +58,8 @@ CREATE TABLE IF NOT EXISTS study_sessions ( -- AUTO-UPDATE TIMESTAMPS -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• +-- update_updated_at: automatically sets updated_at to the current timestamp +-- whenever a row is modified. Attached as a BEFORE UPDATE trigger on each table. CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN @@ -141,7 +143,8 @@ CREATE INDEX IF NOT EXISTS idx_sessions_topic ON study_sessions(topic); -- USEFUL VIEWS -- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• --- Daily completion summary +-- daily_summary: shows which journal fields (learnt, gratitude, verse_reflection) +-- have been filled in for each user/day combination. Useful for habit tracking UI. CREATE OR REPLACE VIEW daily_summary AS SELECT user_id, @@ -151,7 +154,8 @@ SELECT verse_reflection IS NOT NULL AND verse_reflection != '' AS has_verse FROM journal; --- Study streak (how many consecutive days journalled per user) +-- journal_streak: returns total journalled days and the last entry date per user. +-- Note: counts all distinct days journalled, not necessarily consecutive days. CREATE OR REPLACE VIEW journal_streak AS SELECT user_id, diff --git a/frontend/api.js b/frontend/api.js index 624ca9c..4523d24 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -201,6 +201,12 @@ const API = (() => { const state = pRes.data?.state_data || {}; const tasks = tRes.data || []; const journal = jRes.data || []; + // Days remaining until 30 September of the current academic year. + // If September has already passed this calendar year, target next year's date. + const now = new Date(); + const targetYear = now.getMonth() >= 8 ? now.getFullYear() + 1 : now.getFullYear(); + const septTarget = new Date(targetYear, 8, 30); // month 8 = September (0-indexed) + const daysUntilSept = Math.max(0, Math.floor((septTarget - now) / 86400000)); return { schedule_tasks_done: Object.keys(state).filter(k => k.startsWith('chk_') && state[k]).length, roadmap_steps_done: Object.keys(state).filter(k => k.startsWith('rm_') && state[k]).length, @@ -210,7 +216,7 @@ const API = (() => { custom_tasks_done: tasks.filter(t => t.done).length, journal_entries: journal.length, last_active: pRes.data?.updated_at || null, - days_until_sept: Math.max(0, Math.floor((new Date(new Date().getFullYear() + (new Date().getMonth() >= 8 ? 1 : 0), 8, 30) - new Date()) / 86400000)) + days_until_sept: daysUntilSept }; } catch {} }