Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
25dedba
Refactor: Migrate from course_name strings to course_id FKs
Apr 14, 2026
c4ecd02
Revert "Fix calendar assignment type mismatch from nullable course na…
Jose-Gael-Cruz-Lopez Apr 14, 2026
438ab73
Merge PR #52 into combined migration + calendar fix
Jose-Gael-Cruz-Lopez Apr 14, 2026
db72cd3
fix(calendar): deterministic fallback id for normalized assignments
Jose-Gael-Cruz-Lopez Apr 14, 2026
4849eab
fix(api): correct extractSyllabus error status ref; align addCourse t…
Jose-Gael-Cruz-Lopez Apr 14, 2026
20d59fd
feat(frontend): align pages with course_id API; extend assignment nor…
Jose-Gael-Cruz-Lopez Apr 14, 2026
a46692f
Fix CodeRabbit issues: NameError, HTTP 200 on error, wrong course_id …
Jose-Gael-Cruz-Lopez Apr 14, 2026
5434504
fix: quiz course context shape, calendar legacy labels, session cours…
Jose-Gael-Cruz-Lopez Apr 14, 2026
4e79608
merge main: workflows and latest
Jose-Gael-Cruz-Lopez Apr 14, 2026
60dab9d
fix(ci): merge main, non-blocking Claude steps, align tests with cour…
Jose-Gael-Cruz-Lopez Apr 14, 2026
48e0a1e
fix(course-id): per-concept quiz stats, scoped graph updates, syllabu…
Jose-Gael-Cruz-Lopez Apr 14, 2026
842d01e
Fix CodeRabbit review issues: course context payload shape, silent er…
Jose-Gael-Cruz-Lopez Apr 15, 2026
71fd315
Resolve rebase conflicts: merge remote label-matching and logging fixes
Jose-Gael-Cruz-Lopez Apr 15, 2026
f23816c
Fix remaining CodeRabbit issues: unused vars, semester derivation, st…
Jose-Gael-Cruz-Lopez Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:

- name: Run Claude Code Review
id: claude-review
continue-on-error: true
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:

- name: Run Claude Code
id: claude
continue-on-error: true
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Expand Down
231 changes: 159 additions & 72 deletions backend/db/supabase_schema.sql
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
-- ============================================================
-- Sapling — Supabase Schema
-- Sapling — Supabase Schema (course_id migration)
-- Run this in: Supabase Dashboard → SQL Editor → New query
-- ============================================================

-- Users
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
streak_count INTEGER DEFAULT 0,
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
streak_count INTEGER DEFAULT 0,
last_active_date TEXT,
room_id TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
google_id TEXT UNIQUE,
avatar_url TEXT,
auth_provider TEXT DEFAULT 'google'
room_id TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
google_id TEXT UNIQUE,
avatar_url TEXT,
auth_provider TEXT DEFAULT 'google'
);

CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id);

-- Canonical course catalog (no user_id — shared across all students)
CREATE TABLE IF NOT EXISTS courses (
id TEXT PRIMARY KEY,
course_code TEXT NOT NULL,
course_name TEXT NOT NULL,
department TEXT,
credits INTEGER,
semester TEXT DEFAULT 'Spring 2026',
instructor_name TEXT,
meeting_times TEXT,
location TEXT,
description TEXT,
syllabus_url TEXT,
school TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

-- Enrollment join table (user ↔ canonical course)
CREATE TABLE IF NOT EXISTS user_courses (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
course_id TEXT NOT NULL REFERENCES courses(id),
color TEXT,
nickname TEXT,
enrolled_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (user_id, course_id)
);

CREATE INDEX IF NOT EXISTS idx_user_courses_user_id ON user_courses(user_id);

-- Knowledge graph nodes
CREATE TABLE IF NOT EXISTS graph_nodes (
id TEXT PRIMARY KEY,
Expand All @@ -29,10 +59,13 @@ CREATE TABLE IF NOT EXISTS graph_nodes (
times_studied INTEGER DEFAULT 0,
last_studied_at TIMESTAMPTZ,
subject TEXT,
course_id TEXT REFERENCES courses(id),
created_at TIMESTAMPTZ DEFAULT now(),
mastery_events JSONB DEFAULT '[]' -- array of {ts, delta, reason, event_type} — last 20 events
mastery_events JSONB DEFAULT '[]'
);

CREATE INDEX IF NOT EXISTS idx_graph_nodes_user_course ON graph_nodes(user_id, course_id);

-- Knowledge graph edges
CREATE TABLE IF NOT EXISTS graph_edges (
id TEXT PRIMARY KEY,
Expand All @@ -41,42 +74,30 @@ CREATE TABLE IF NOT EXISTS graph_edges (
target_node_id TEXT NOT NULL REFERENCES graph_nodes(id),
strength DOUBLE PRECISION DEFAULT 0.5,
created_at TIMESTAMPTZ DEFAULT now(),
relationship_type TEXT DEFAULT 'related' -- 'prerequisite' | 'builds_on' | 'related'
);

-- Migrations (run these if the table already exists)
-- ALTER TABLE graph_nodes ADD COLUMN IF NOT EXISTS mastery_events JSONB DEFAULT '[]';
-- ALTER TABLE graph_edges ADD COLUMN IF NOT EXISTS relationship_type TEXT DEFAULT 'related';

-- Courses
CREATE TABLE IF NOT EXISTS courses (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
course_name TEXT NOT NULL,
color TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (user_id, course_name)
relationship_type TEXT DEFAULT 'related'
);

-- Learning sessions
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
mode TEXT NOT NULL,
topic TEXT NOT NULL,
started_at TIMESTAMPTZ DEFAULT now(),
ended_at TIMESTAMPTZ,
summary_json JSONB
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
mode TEXT NOT NULL,
topic TEXT NOT NULL,
course_id TEXT REFERENCES courses(id),
started_at TIMESTAMPTZ DEFAULT now(),
ended_at TIMESTAMPTZ,
summary_json JSONB,
name TEXT
);

-- Chat messages within a session
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT NOT NULL,
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL REFERENCES sessions(id),
role TEXT NOT NULL,
content TEXT NOT NULL,
graph_update_json JSONB,
created_at TIMESTAMPTZ DEFAULT now()
created_at TIMESTAMPTZ DEFAULT now()
);

-- Quiz attempts
Expand Down Expand Up @@ -107,14 +128,72 @@ CREATE TABLE IF NOT EXISTS assignments (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
course_name TEXT,
course_id TEXT REFERENCES courses(id),
due_date TEXT NOT NULL,
assignment_type TEXT,
notes TEXT,
google_event_id TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_assignments_user_due ON assignments(user_id, due_date);

-- Documents (uploaded course materials)
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
course_id TEXT NOT NULL REFERENCES courses(id),
file_name TEXT NOT NULL,
category TEXT NOT NULL,
summary TEXT,
key_takeaways JSONB,
flashcards JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
processed_at TIMESTAMPTZ
);

-- Study guides
CREATE TABLE IF NOT EXISTS study_guides (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
user_id TEXT NOT NULL REFERENCES users(id),
course_id TEXT NOT NULL REFERENCES courses(id),
exam_id TEXT NOT NULL,
generated_at TIMESTAMPTZ DEFAULT now(),
content JSONB NOT NULL
);

-- Per-concept aggregated course stats (across all enrolled students)
CREATE TABLE IF NOT EXISTS course_concept_stats (
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::TEXT,
course_id TEXT NOT NULL REFERENCES courses(id),
concept_name TEXT NOT NULL,
semester TEXT NOT NULL DEFAULT 'Spring 2026',
student_count INTEGER DEFAULT 0,
avg_mastery_score DOUBLE PRECISION DEFAULT 0.0,
pct_mastered DOUBLE PRECISION DEFAULT 0.0,
pct_struggling DOUBLE PRECISION DEFAULT 0.0,
pct_unexplored DOUBLE PRECISION DEFAULT 0.0,
common_misconceptions TEXT[] DEFAULT '{}',
effective_explanations TEXT[] DEFAULT '{}',
prerequisite_gaps TEXT[] DEFAULT '{}',
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (course_id, concept_name, semester)
);

-- Course-wide summary (rolled up from course_concept_stats)
CREATE TABLE IF NOT EXISTS course_summary (
course_id TEXT NOT NULL REFERENCES courses(id),
semester TEXT NOT NULL DEFAULT 'Spring 2026',
student_count INTEGER DEFAULT 0,
avg_class_mastery DOUBLE PRECISION DEFAULT 0.0,
top_struggling_concepts TEXT[] DEFAULT '{}',
top_mastered_concepts TEXT[] DEFAULT '{}',
summary_text TEXT,
summary_hash TEXT,
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (course_id, semester)
);

-- Study rooms
CREATE TABLE IF NOT EXISTS rooms (
id TEXT PRIMARY KEY,
Expand Down Expand Up @@ -169,21 +248,16 @@ CREATE INDEX IF NOT EXISTS idx_room_messages_room_id ON room_messages(room_id, c

-- Emoji reactions on room messages
CREATE TABLE IF NOT EXISTS room_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES room_messages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id),
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES room_messages(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id),
emoji TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(message_id, user_id, emoji)
);

CREATE INDEX IF NOT EXISTS idx_room_reactions_message_id ON room_reactions(message_id);

-- Migrations (run if tables already exist without these columns):
-- ALTER TABLE room_messages ADD COLUMN IF NOT EXISTS reply_to_id UUID REFERENCES room_messages(id);
-- ALTER TABLE room_messages ADD COLUMN IF NOT EXISTS is_deleted BOOLEAN NOT NULL DEFAULT FALSE;
-- ALTER TABLE room_messages ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ;

-- Cached AI summaries for study rooms
CREATE TABLE IF NOT EXISTS room_summaries (
room_id TEXT PRIMARY KEY REFERENCES rooms(id),
Expand All @@ -192,14 +266,7 @@ CREATE TABLE IF NOT EXISTS room_summaries (
updated_at TIMESTAMPTZ DEFAULT now()
);

-- Shared course-level learning context (aggregated from all students, no Gemini)
CREATE TABLE IF NOT EXISTS course_context (
course_name TEXT PRIMARY KEY,
context_json JSONB NOT NULL,
student_count INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT now()
);

-- Flashcards
CREATE TABLE IF NOT EXISTS flashcards (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
Expand All @@ -214,18 +281,38 @@ CREATE TABLE IF NOT EXISTS flashcards (

CREATE INDEX IF NOT EXISTS idx_flashcards_user_topic ON flashcards(user_id, topic);

CREATE TABLE public.flashcards (
id text NOT NULL,
user_id text NOT NULL,
topic text NOT NULL,
front text NOT NULL,
back text NOT NULL,
times_reviewed integer DEFAULT 0,
last_rating integer,
last_reviewed_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now(),
CONSTRAINT flashcards_pkey PRIMARY KEY (id),
CONSTRAINT flashcards_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id)
);

CREATE INDEX IF NOT EXISTS idx_flashcards_user_topic ON public.flashcards(user_id, topic);
-- Feedback
CREATE TABLE IF NOT EXISTS feedback (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
rating INTEGER NOT NULL,
selected_options JSONB DEFAULT '[]',
comment TEXT,
session_id TEXT,
topic TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);

-- Issue reports
CREATE TABLE IF NOT EXISTS issue_reports (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
topic TEXT NOT NULL,
description TEXT NOT NULL,
screenshot_urls JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT now()
);

-- Job applications
CREATE TABLE IF NOT EXISTS job_applications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
position TEXT NOT NULL,
full_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
linkedin_url TEXT NOT NULL,
resume TEXT,
portfolio_link TEXT,
submitted_at TIMESTAMPTZ DEFAULT now()
);
7 changes: 5 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

from config import FRONTEND_URL, PORT
from routes import graph, learn, quiz, calendar, social, extract, auth, documents, flashcards, study_guide, feedback, careers
from recost.frameworks.fastapi import RecostMiddleware

try:
from recost.frameworks.fastapi import RecostMiddleware
except ImportError:
RecostMiddleware = None # optional; tests/CI without recost package

load_dotenv(Path(__file__).with_name(".env"))

Expand All @@ -17,7 +20,7 @@

app = FastAPI(title="Sapling API", version="1.0.0")

if recost_api_key:
if recost_api_key and RecostMiddleware is not None:
app.add_middleware(
RecostMiddleware,
api_key=recost_api_key,
Expand Down
Loading
Loading