#!/usr/bin/env bash # Seeds synthetic reading sessions and daily stats for the first user so that # all user-statistics charts (heatmap, peak hours, completion timeline, etc.) # show realistic data out of the box. # # Safe to run multiple times - sessions are deduplicated by session_id and # daily stats are upserted. Running again adds more data on top. # # Usage: ./scripts/fixtures/seed-reading-sessions.sh [USER_ID] # USER_ID defaults to 1. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT_DIR" POSTGRES_USER="${POSTGRES_USER:-bookorbit}" POSTGRES_DB="${POSTGRES_DB:-bookorbit}" TARGET_USER_ID="${1:-1}" echo "Seeding reading sessions for user_id=${TARGET_USER_ID}..." docker compose -f docker-compose.dev.yml exec -T postgres psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" << EOSQL DO \$\$ DECLARE v_user_id int := ${TARGET_USER_ID}; v_day date; v_file_id int; v_library_id int; v_sessions int; v_i int; v_hour int; v_duration int; v_started timestamptz; v_ended timestamptz; v_end_pct real; v_delta real; v_session_id text; v_dow int; v_file_ids int[]; v_total_sessions int := 0; v_total_completions int := 0; v_inserted_sessions int := 0; v_inserted int; v_is_completion boolean; BEGIN -- Verify user exists IF NOT EXISTS (SELECT 1 FROM users WHERE id = v_user_id) THEN RAISE EXCEPTION 'User with id % not found', v_user_id; END IF; -- Verify there are book files to work with IF NOT EXISTS (SELECT 1 FROM book_files LIMIT 1) THEN RAISE EXCEPTION 'No book files found - scan a library first'; END IF; -- Pick up to 40 real file ids to spread sessions across SELECT array_agg(id ORDER BY random()) INTO v_file_ids FROM (SELECT id FROM book_files ORDER BY random() LIMIT 40) t; -- Spread books.added_at across the past 3 years so Format Share Over Time -- chart has data across multiple months instead of a single spike today. UPDATE books SET added_at = now() - (random() * interval '3 years') WHERE added_at > now() - interval '7 days'; -- Insert sessions across the past 365 days FOR v_day IN SELECT generate_series( current_date - interval '364 days', current_date, interval '1 day' )::date LOOP v_dow := extract(dow from v_day); -- 0=Sun 6=Sat -- Weekends: 1-3 sessions. Weekdays: 0-2 (30% chance of 0). IF v_dow IN (0, 6) THEN v_sessions := 1 + floor(random() * 3)::int; ELSIF random() < 0.30 THEN v_sessions := 0; ELSE v_sessions := 1 + floor(random() * 2)::int; END IF; FOR v_i IN 1..v_sessions LOOP v_file_id := v_file_ids[1 + floor(random() * array_length(v_file_ids, 1))::int]; SELECT b.library_id INTO v_library_id FROM book_files bf JOIN books b ON b.id = bf.book_id WHERE bf.id = v_file_id; -- Realistic hour distribution: mostly evenings, some mornings/afternoons IF random() < 0.60 THEN v_hour := 18 + floor(random() * 6)::int; -- 18-23 evening ELSIF random() < 0.50 THEN v_hour := 7 + floor(random() * 3)::int; -- 7-9 morning ELSE v_hour := 12 + floor(random() * 4)::int; -- 12-15 afternoon END IF; v_started := (v_day + make_interval(hours => v_hour, mins => floor(random() * 60)::int))::timestamptz; v_duration := 600 + floor(random() * 3000)::int; -- 10-60 min v_ended := v_started + make_interval(secs => v_duration); v_delta := round((1 + random() * 8)::numeric, 4); -- 1-9% progress -- ~15% of sessions are completions (every 7th deterministically by loop) IF (v_total_sessions % 7) = 0 THEN v_end_pct := 100; v_is_completion := true; ELSE v_end_pct := round((10 + random() * 80)::numeric, 2); v_is_completion := false; END IF; v_session_id := md5(v_user_id::text || v_file_id::text || v_started::text || v_i::text); v_total_sessions := v_total_sessions + 1; INSERT INTO reading_sessions (user_id, book_file_id, session_id, started_at, ended_at, duration_seconds, progress_delta, end_progress) VALUES (v_user_id, v_file_id, v_session_id, v_started, v_ended, v_duration, v_delta, v_end_pct) ON CONFLICT (user_id, session_id) DO NOTHING; GET DIAGNOSTICS v_inserted = ROW_COUNT; IF v_inserted = 0 THEN CONTINUE; END IF; -- Real-time upsert of daily stats (mirrors what the server does on session save) INSERT INTO user_reading_daily_stats (user_id, library_id, day, reading_seconds, progress_delta, sessions_count, updated_at) VALUES (v_user_id, v_library_id, v_day, v_duration, v_delta, 1, now()) ON CONFLICT (user_id, library_id, day) DO UPDATE SET reading_seconds = user_reading_daily_stats.reading_seconds + excluded.reading_seconds, progress_delta = user_reading_daily_stats.progress_delta + excluded.progress_delta, sessions_count = user_reading_daily_stats.sessions_count + 1, updated_at = now(); v_inserted_sessions := v_inserted_sessions + 1; IF v_is_completion THEN v_total_completions := v_total_completions + 1; END IF; END LOOP; END LOOP; RAISE NOTICE 'Done: % sessions generated, % inserted (% completions)', v_total_sessions, v_inserted_sessions, v_total_completions; END; \$\$; EOSQL echo "Reading session seed complete."