From 38f8befb1c6eb7918b83406bfbc2b1580753ca33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:08:00 +0000 Subject: [PATCH 1/5] Refresh verified exam session dates Agent-Logs-Url: https://github.com/kewonit/Timekeeper/sessions/e5688765-3683-4d02-8978-abadcb23cec7 Co-authored-by: kewonit <108450560+kewonit@users.noreply.github.com> --- src/data/exam-sessions.json | 263 +++++++++++++++++++----------------- 1 file changed, 142 insertions(+), 121 deletions(-) diff --git a/src/data/exam-sessions.json b/src/data/exam-sessions.json index 840ea51..f58b588 100644 --- a/src/data/exam-sessions.json +++ b/src/data/exam-sessions.json @@ -7,13 +7,13 @@ "session": "Session 1", "date": "2026-01-21", "endDate": "2026-01-30", - "note": "Official NTA dates confirmed" + "note": "Official NTA schedule" }, { "session": "Session 2", - "date": "2026-04-02", - "endDate": "2026-04-09", - "note": "Official NTA dates confirmed" + "date": "2026-04-01", + "endDate": "2026-04-10", + "note": "Official NTA schedule" } ] }, @@ -33,7 +33,7 @@ { "session": "Single Session", "date": "2026-05-03", - "note": "Expected first Sunday of May - NTA notification awaited" + "note": "Official NTA public notice" } ] }, @@ -43,14 +43,14 @@ { "session": "Session 1", "date": "2026-04-15", - "endDate": "2026-04-17", - "note": "Official BITSAT dates" + "endDate": "2026-04-16", + "note": "Official BITSAT schedule" }, { "session": "Session 2", "date": "2026-05-24", "endDate": "2026-05-26", - "note": "Official BITSAT dates" + "note": "Official BITSAT schedule" } ] }, @@ -69,13 +69,13 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-05-31", - "note": "Expected last Sunday of May - UPSC calendar awaited" + "date": "2026-05-24", + "note": "Official UPSC annual calendar 2026" }, { "session": "Main", - "date": "2026-09-18", - "note": "Expected mid-September - UPSC calendar awaited" + "date": "2026-08-21", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -136,7 +136,7 @@ { "session": "Single Session", "date": "2025-12-07", - "note": "Expected first Sunday of December 2025 for 2026 Admission" + "note": "Official Consortium of NLUs schedule for CLAT 2026" } ] }, @@ -145,8 +145,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-04-26", - "note": "Expected late April - WBJEEB notification awaited" + "date": "2026-05-24", + "note": "Official WBJEEB schedule" } ] }, @@ -156,14 +156,14 @@ { "session": "PCM", "date": "2026-04-11", - "endDate": "2026-04-19", - "note": "Official MHT CET dates" + "endDate": "2026-04-20", + "note": "Official Maharashtra CET Cell schedule" }, { "session": "PCB", "date": "2026-04-21", "endDate": "2026-04-26", - "note": "Official MHT CET dates" + "note": "Official Maharashtra CET Cell schedule" } ] }, @@ -171,10 +171,19 @@ "id": "kcet-2026", "sessions": [ { - "session": "Single Session", - "date": "2026-04-18", - "endDate": "2026-04-19", - "note": "Expected mid-April - KEA notification awaited" + "session": "Kannada Language Test", + "date": "2026-04-22", + "note": "Official KEA schedule" + }, + { + "session": "Paper 1", + "date": "2026-04-23", + "note": "Official KEA schedule" + }, + { + "session": "Paper 2", + "date": "2026-04-24", + "note": "Official KEA schedule" } ] }, @@ -183,8 +192,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-05-10", - "note": "Expected second Sunday of May - COMEDK notification awaited" + "date": "2026-05-09", + "note": "Official COMEDK notification" } ] }, @@ -234,9 +243,9 @@ "sessions": [ { "session": "Testing Window", - "date": "2025-11-05", - "endDate": "2025-12-19", - "note": "Official NMAT dates" + "date": "2025-09-18", + "endDate": "2026-01-31", + "note": "Official GMAC NMAT testing window" } ] }, @@ -245,14 +254,14 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-10-03", - "endDate": "2026-10-10", - "note": "Expected October - IBPS calendar awaited" + "date": "2026-08-22", + "endDate": "2026-08-23", + "note": "Official IBPS calendar 2026-27" }, { "session": "Main", - "date": "2026-11-28", - "note": "Expected November - IBPS calendar awaited" + "date": "2026-10-04", + "note": "Official IBPS calendar 2026-27" } ] }, @@ -340,14 +349,14 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-08-22", - "endDate": "2026-08-29", - "note": "Expected August - IBPS calendar awaited" + "date": "2026-10-10", + "endDate": "2026-10-11", + "note": "Official IBPS CSA calendar 2026-27" }, { "session": "Main", - "date": "2026-10-03", - "note": "Expected October - IBPS calendar awaited" + "date": "2026-12-27", + "note": "Official IBPS CSA calendar 2026-27" } ] }, @@ -356,8 +365,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-04-19", - "note": "Expected third Sunday of April - UPSC calendar awaited" + "date": "2026-04-12", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -367,7 +376,7 @@ { "session": "Single Session", "date": "2026-04-12", - "note": "Expected April - UPSC calendar awaited" + "note": "Official UPSC annual calendar 2026" } ] }, @@ -376,13 +385,13 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-05-31", - "note": "Combined with UPSC CSE Prelims - UPSC calendar awaited" + "date": "2026-05-24", + "note": "Official UPSC annual calendar 2026" }, { "session": "Main", "date": "2026-11-22", - "note": "Expected November - UPSC calendar awaited" + "note": "Official UPSC annual calendar 2026" } ] }, @@ -391,13 +400,13 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-02-15", - "note": "Expected February - UPSC calendar awaited" + "date": "2026-02-08", + "note": "Official UPSC annual calendar 2026" }, { "session": "Main", "date": "2026-06-21", - "note": "Expected June - UPSC calendar awaited" + "note": "Official UPSC annual calendar 2026" } ] }, @@ -406,8 +415,13 @@ "sessions": [ { "session": "Preliminary", - "date": "2026-02-15", - "note": "Combined with ESE - UPSC calendar awaited" + "date": "2026-02-08", + "note": "Official UPSC annual calendar 2026" + }, + { + "session": "Main", + "date": "2026-06-20", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -416,18 +430,25 @@ "sessions": [ { "session": "Officer Scale I Prelims", - "date": "2026-08-08", - "note": "Expected August - IBPS calendar awaited" + "date": "2026-11-21", + "endDate": "2026-11-22", + "note": "Official IBPS RRB calendar 2026-27" + }, + { + "session": "Officer Scale I Main", + "date": "2026-12-20", + "note": "Official IBPS RRB calendar 2026-27" }, { "session": "Office Assistant Prelims", - "date": "2026-08-15", - "note": "Expected August - IBPS calendar awaited" + "date": "2026-12-06", + "endDate": "2026-12-13", + "note": "Official IBPS RRB calendar 2026-27" }, { - "session": "Mains", - "date": "2026-09-26", - "note": "Expected September - IBPS calendar awaited" + "session": "Office Assistant Main", + "date": "2027-01-30", + "note": "Official IBPS RRB calendar 2026-27" } ] }, @@ -484,7 +505,7 @@ { "session": "Single Session", "date": "2026-06-07", - "note": "Expected first Sunday of June - IISER notification awaited" + "note": "Official IISER Admissions schedule" } ] }, @@ -493,8 +514,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-06-14", - "note": "Expected mid-June - NISER notification awaited" + "date": "2026-06-06", + "note": "Official NEST 2026 schedule" } ] }, @@ -503,13 +524,13 @@ "sessions": [ { "session": "INI CET January", - "date": "2026-01-11", - "note": "Expected second Sunday of January - AIIMS notification awaited" + "date": "2025-11-09", + "note": "Official AIIMS exam calendar" }, { "session": "INI CET July", - "date": "2026-07-12", - "note": "Expected second Sunday of July - AIIMS notification awaited" + "date": "2026-05-16", + "note": "Official AIIMS exam calendar" } ] }, @@ -538,15 +559,14 @@ "sessions": [ { "session": "AFCAT 1", - "date": "2026-02-21", - "endDate": "2026-02-23", - "note": "Expected Feb 2026 - IAF notification awaited" + "date": "2026-01-31", + "note": "Official AFCAT 01/2026 notification" }, { "session": "AFCAT 2", "date": "2026-08-22", "endDate": "2026-08-24", - "note": "Expected Aug 2026 - IAF notification awaited" + "note": "Official AFCAT 2 2026 date not announced yet; existing placeholder retained" } ] }, @@ -570,8 +590,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-08-02", - "note": "Expected first Sunday of August - UPSC notification awaited" + "date": "2026-07-19", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -580,8 +600,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-09-06", - "note": "Expected first Sunday of September - UPSC notification awaited" + "date": "2026-09-13", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -590,8 +610,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-11-08", - "note": "Expected November 2026 - UPSC notification awaited" + "date": "2026-09-13", + "note": "Official UPSC annual calendar 2026" } ] }, @@ -600,21 +620,21 @@ "sessions": [ { "session": "Phase 1", - "date": "2026-04-23", - "endDate": "2026-04-28", - "note": "Official SRMJEE dates" + "date": "2026-04-24", + "endDate": "2026-04-29", + "note": "Official SRMIST schedule" }, { "session": "Phase 2", "date": "2026-06-10", "endDate": "2026-06-15", - "note": "Official SRMJEE dates" + "note": "Official SRMIST schedule" }, { "session": "Phase 3", "date": "2026-07-04", "endDate": "2026-07-05", - "note": "Official SRMJEE dates" + "note": "Official SRMIST schedule" } ] }, @@ -623,9 +643,8 @@ "sessions": [ { "session": "Single Session", - "date": "2026-05-15", - "endDate": "2026-05-20", - "note": "Expected mid-May - AMU notification awaited" + "date": "2026-04-19", + "note": "Official AMU Controller of Examinations schedule" } ] }, @@ -643,16 +662,14 @@ "id": "cuet-ug-2026", "sessions": [ { - "session": "Phase 1", - "date": "2026-05-15", - "endDate": "2026-05-31", - "note": "Expected mid-May to end-May - NTA notification awaited" + "session": "Exam Window Starts", + "date": "2026-05-11", + "note": "Official NTA exam window" }, { - "session": "Phase 2", - "date": "2026-06-01", - "endDate": "2026-06-15", - "note": "Expected early June - NTA notification awaited" + "session": "Exam Window Ends", + "date": "2026-05-31", + "note": "Official NTA exam window" } ] }, @@ -660,10 +677,14 @@ "id": "cuet-pg-2026", "sessions": [ { - "session": "Single Session", - "date": "2026-03-10", - "endDate": "2026-03-28", - "note": "Official CUET PG dates" + "session": "Exam Window Starts", + "date": "2026-03-06", + "note": "Official NTA subject-wise schedule" + }, + { + "session": "Exam Window Ends", + "date": "2026-03-27", + "note": "Official NTA subject-wise schedule" } ] }, @@ -672,13 +693,13 @@ "sessions": [ { "session": "Main Exam Start", - "date": "2026-02-15", - "note": "Expected mid-February - CBSE datesheet awaited" + "date": "2026-02-17", + "note": "Official CBSE date sheet" }, { "session": "Main Exam End", - "date": "2026-03-20", - "note": "Expected mid-March - CBSE datesheet awaited" + "date": "2026-03-10", + "note": "Official CBSE date sheet" } ] }, @@ -687,13 +708,13 @@ "sessions": [ { "session": "Main Exam Start", - "date": "2026-02-15", - "note": "Expected mid-February - CBSE datesheet awaited" + "date": "2026-02-17", + "note": "Official CBSE date sheet" }, { "session": "Main Exam End", - "date": "2026-04-15", - "note": "Expected mid-April - CBSE datesheet awaited" + "date": "2026-04-09", + "note": "Official CBSE date sheet" } ] }, @@ -702,13 +723,13 @@ "sessions": [ { "session": "Main Exam Start", - "date": "2026-02-20", - "note": "Expected late February - CISCE datesheet awaited" + "date": "2026-02-17", + "note": "Official CISCE date sheet" }, { "session": "Main Exam End", "date": "2026-03-30", - "note": "Expected late March - CISCE datesheet awaited" + "note": "Official CISCE date sheet" } ] }, @@ -718,12 +739,12 @@ { "session": "Main Exam Start", "date": "2026-02-12", - "note": "Expected mid-February - CISCE datesheet awaited" + "note": "Official CISCE date sheet" }, { "session": "Main Exam End", - "date": "2026-04-10", - "note": "Expected early April - CISCE datesheet awaited" + "date": "2026-04-06", + "note": "Official CISCE date sheet" } ] }, @@ -732,13 +753,13 @@ "sessions": [ { "session": "Main Exam Start", - "date": "2026-02-21", - "note": "Expected late February - Maharashtra Board datesheet awaited" + "date": "2026-02-10", + "note": "Official Maharashtra Board timetable" }, { "session": "Main Exam End", - "date": "2026-03-20", - "note": "Expected mid-March - Maharashtra Board datesheet awaited" + "date": "2026-03-11", + "note": "Official Maharashtra Board timetable" } ] }, @@ -747,13 +768,13 @@ "sessions": [ { "session": "Main Exam Start", - "date": "2026-03-02", - "note": "Expected early March - Maharashtra Board datesheet awaited" + "date": "2026-02-20", + "note": "Official Maharashtra Board timetable" }, { "session": "Main Exam End", - "date": "2026-03-25", - "note": "Expected late March - Maharashtra Board datesheet awaited" + "date": "2026-03-18", + "note": "Official Maharashtra Board timetable" } ] }, @@ -763,12 +784,12 @@ { "session": "Main Exam Start", "date": "2026-02-18", - "note": "Expected mid-February - UP Board datesheet awaited" + "note": "Official UPMSP timetable" }, { "session": "Main Exam End", - "date": "2026-03-10", - "note": "Expected early March - UP Board datesheet awaited" + "date": "2026-03-12", + "note": "Official UPMSP timetable" } ] }, @@ -778,14 +799,14 @@ { "session": "Main Exam Start", "date": "2026-02-18", - "note": "Expected mid-February - UP Board datesheet awaited" + "note": "Official UPMSP timetable" }, { "session": "Main Exam End", - "date": "2026-03-20", - "note": "Expected mid-March - UP Board datesheet awaited" + "date": "2026-03-12", + "note": "Official UPMSP timetable" } ] } ] -} \ No newline at end of file +} From 6c651a6fe35f8a0e18af1120b953990e36ca8ac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:28:56 +0000 Subject: [PATCH 2/5] Roll over expired exams to next upcoming sessions Agent-Logs-Url: https://github.com/kewonit/Timekeeper/sessions/3d6f935d-4fcf-498e-8c59-690661fda4a8 Co-authored-by: kewonit <108450560+kewonit@users.noreply.github.com> --- src/components/FAQSchema.astro | 9 +++-- src/components/JsonLD.astro | 5 ++- src/components/SEOHead.astro | 24 +++++++----- src/data/exam-data.ts | 70 ++++++++++++++++++++++++++++++---- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/components/FAQSchema.astro b/src/components/FAQSchema.astro index e630a08..2bd1faa 100644 --- a/src/components/FAQSchema.astro +++ b/src/components/FAQSchema.astro @@ -5,14 +5,15 @@ interface Props { } const { currentPage = 'home', exam } = Astro.props; +const examYear = exam?.sessions?.[0]?.date ? new Date(exam.sessions[0].date).getFullYear() : 2026; // Generate FAQ data based on context const generateFAQs = (context: string, exam?: any) => { if (context === 'exam' && exam) { return [ { - question: `When is ${exam.name} 2026 exam?`, - answer: `${exam.fullName} 2026 is scheduled for ${new Date(exam.sessions?.[0]?.date || exam.date).toLocaleDateString('en-IN', { + question: `When is ${exam.name} ${examYear} exam?`, + answer: `${exam.fullName} ${examYear} is scheduled for ${new Date(exam.sessions?.[0]?.date || exam.date).toLocaleDateString('en-IN', { day: 'numeric', month: 'long', year: 'numeric' @@ -32,7 +33,7 @@ const generateFAQs = (context: string, exam?: any) => { }, { question: `Does the countdown timer work offline?`, - answer: `Yes, our countdown timer works offline once loaded. The timer continues to run even without internet connection, ensuring you never lose track of time until ${exam.name} 2026.` + answer: `Yes, our countdown timer works offline once loaded. The timer continues to run even without internet connection, ensuring you never lose track of time until ${exam.name} ${examYear}.` }, { question: `What subjects are covered in ${exam.name}?`, @@ -120,4 +121,4 @@ const schema = { ))} - \ No newline at end of file + diff --git a/src/components/JsonLD.astro b/src/components/JsonLD.astro index 6dce317..84916da 100644 --- a/src/components/JsonLD.astro +++ b/src/components/JsonLD.astro @@ -81,6 +81,7 @@ if (type === 'website') { const examUrl = `${baseUrl}/exams/${exam.slug}`; const currentSession = exam.sessions?.[0]; const examDate = currentSession?.date || exam.date; + const examYear = new Date(examDate).getFullYear(); structuredData = { "@context": "https://schema.org", @@ -122,7 +123,7 @@ if (type === 'website') { "@type": "WebPage", "@id": `${examUrl}/#webpage`, "url": examUrl, - "name": `${exam.name} 2026 Countdown Timer | ${exam.fullName}`, + "name": `${exam.name} ${examYear} Countdown Timer | ${exam.fullName}`, "description": exam.metaDescription, "isPartOf": { "@id": `${baseUrl}/#website` @@ -198,4 +199,4 @@ if (type === 'website') { } --- - \ No newline at end of file + diff --git a/src/components/SEOHead.astro b/src/components/SEOHead.astro index 1556be5..47db6f1 100644 --- a/src/components/SEOHead.astro +++ b/src/components/SEOHead.astro @@ -6,14 +6,18 @@ interface Props { const { exam, currentUrl = Astro.url.href } = Astro.props; const baseUrl = 'https://timekeeper.edbn.me'; +const sessionYear = exam?.sessions?.[0]?.date ? new Date(exam.sessions[0].date).getFullYear() : 2026; // Helper to calculate time left -function calculateTimeLeft(targetDate: string) { - if (!targetDate) { +function calculateTimeLeft(session?: { date?: string; endDate?: string }) { + if (!session?.date) { return { expired: true, days: 0, hours: 0, minutes: 0, seconds: 0 }; } + const now = new Date().getTime(); - const target = new Date(targetDate).getTime(); + const start = new Date(session.date).getTime(); + const end = new Date(session.endDate || session.date).getTime(); + const target = now < start ? start : end; const distance = target - now; if (distance < 0) { @@ -31,16 +35,16 @@ function calculateTimeLeft(targetDate: string) { // Generate dynamic title function generateDynamicTitle(exam?: any): string { if (exam) { - const timeLeft = calculateTimeLeft(exam.sessions?.[0]?.date || exam.date); + const timeLeft = calculateTimeLeft(exam.sessions?.[0] || (exam.date ? { date: exam.date } : undefined)); if (timeLeft.expired) { - return `${exam.name} 2026 Exam Date & Results - TimeKeeper`; + return `${exam.name} ${sessionYear} Exam Date & Results - TimeKeeper`; } const timeString = timeLeft.days > 0 ? `${timeLeft.days} Days ${timeLeft.hours}h` : `${timeLeft.hours}h ${timeLeft.minutes}m`; - return `[${timeString} Left] ${exam.name} Countdown Timer 2026 | Live Exam Date Tracker`; + return `[${timeString} Left] ${exam.name} Countdown Timer ${sessionYear} | Live Exam Date Tracker`; } return 'TimeKeeper - Live Countdown Timer for Indian Exams 2026 | JEE, NEET, CAT'; @@ -49,11 +53,11 @@ function generateDynamicTitle(exam?: any): string { // Generate dynamic meta description function generateDynamicDescription(exam?: any): string { if (exam) { - const timeLeft = calculateTimeLeft(exam.sessions?.[0]?.date || exam.date); + const timeLeft = calculateTimeLeft(exam.sessions?.[0] || (exam.date ? { date: exam.date } : undefined)); const timeString = timeLeft.expired ? 'The exam date has passed. Check for result updates.' : `Only ${timeLeft.days} days, ${timeLeft.hours} hours left!`; - return `⏰ ${timeString} Track the ${exam.name} 2026 countdown LIVE. Our real-time timer shows you the exact time until the ${exam.fullName}. Features Picture-in-Picture mode, multiple session tracking, and shareable links. Free, no sign-up required.`; + return `⏰ ${timeString} Track the ${exam.name} ${sessionYear} countdown LIVE. Our real-time timer shows you the exact time until the ${exam.fullName}. Features Picture-in-Picture mode, multiple session tracking, and shareable links. Free, no sign-up required.`; } return '🏆 Ultimate real-time countdown timer for all major Indian competitive exams 2026. Track JEE Mains, JEE Advanced, NEET, CAT, UPSC, and 50+ other exams. Features live countdowns, PiP mode, and shareable links. Free forever!'; @@ -77,7 +81,7 @@ function generateDynamicKeywords(exam?: any): string[] { const examSpecificKeywords = [ `${exam.name} countdown`, `${exam.name} timer`, - `${exam.name} 2026`, + `${exam.name} ${sessionYear}`, `${exam.name} exam date`, `${exam.name} countdown timer`, `${exam.fullName} countdown`, @@ -145,4 +149,4 @@ const dynamicKeywords = generateDynamicKeywords(exam); - \ No newline at end of file + diff --git a/src/data/exam-data.ts b/src/data/exam-data.ts index e7da782..85fd306 100644 --- a/src/data/exam-data.ts +++ b/src/data/exam-data.ts @@ -5,6 +5,9 @@ import metadataData from './exam-metadata.json'; export interface ExamSession { session: string; date: string; + endDate?: string; + note?: string; + predicted?: boolean; } export interface ExamMetadata { @@ -54,6 +57,53 @@ function initializeLookups(): void { }); } +function getSessionStartTime(session: ExamSession): number { + return new Date(session.date).getTime(); +} + +function getSessionEndTime(session: ExamSession): number { + return new Date(session.endDate || session.date).getTime(); +} + +function shiftISODate(date: string, years: number): string { + const [year, month, day] = date.split('-').map(Number); + return new Date(Date.UTC(year + years, month - 1, day)).toISOString().split('T')[0]; +} + +function predictFutureSessions(sessions: ExamSession[], now: number): ExamSession[] { + for (let yearsToAdvance = 1; yearsToAdvance <= 5; yearsToAdvance++) { + const predictedSessions = sessions.map(session => ({ + ...session, + date: shiftISODate(session.date, yearsToAdvance), + endDate: session.endDate ? shiftISODate(session.endDate, yearsToAdvance) : undefined, + predicted: true, + note: session.note + ? `${session.note} (predicted next cycle from the latest published schedule)` + : 'Predicted next cycle from the latest published schedule' + })); + + if (predictedSessions.some(session => getSessionEndTime(session) >= now)) { + return predictedSessions; + } + } + + return sessions; +} + +function normalizeSessionsForDisplay(sessions: ExamSession[]): ExamSession[] { + if (sessions.length === 0) return []; + + const now = Date.now(); + const sortedSessions = [...sessions].sort((a, b) => getSessionStartTime(a) - getSessionStartTime(b)); + const upcomingOrOngoingSessions = sortedSessions.filter(session => getSessionEndTime(session) >= now); + + if (upcomingOrOngoingSessions.length > 0) { + return upcomingOrOngoingSessions; + } + + return predictFutureSessions(sortedSessions, now); +} + /** * Get combined exam data with intelligent caching */ @@ -69,7 +119,7 @@ export function getExamData(): ExamData[] { examDataCache!.push({ id, ...metadata, - sessions + sessions: normalizeSessionsForDisplay(sessions) }); }); @@ -85,7 +135,7 @@ export function getExamById(id: string): ExamData | undefined { const metadata = metadataLookup!.get(id); if (!metadata) return undefined; - const sessions = sessionsLookup!.get(id) || []; + const sessions = normalizeSessionsForDisplay(sessionsLookup!.get(id) || []); return { id, ...metadata, @@ -161,7 +211,7 @@ export function calculateTimeRemaining(exam: ExamData): { // Calculate time for all sessions const allSessions = exam.sessions.map(session => ({ session, - timeRemaining: calculateSingleTime(session.date, now) + timeRemaining: calculateSingleTime(session.date, now, session.endDate) })); // Find next upcoming session @@ -178,8 +228,10 @@ export function calculateTimeRemaining(exam: ExamData): { /** * Calculate time for a single date */ -function calculateSingleTime(targetDate: string, now: number) { - const target = new Date(targetDate).getTime(); +function calculateSingleTime(targetDate: string, now: number, endDate?: string) { + const start = new Date(targetDate).getTime(); + const end = new Date(endDate || targetDate).getTime(); + const target = now < start ? start : end; const distance = target - now; if (distance < 0) { @@ -199,25 +251,27 @@ function calculateSingleTime(targetDate: string, now: number) { */ export function generateExamPageTitle(exam: ExamData): string { const timeData = calculateTimeRemaining(exam); + const sessionYear = new Date(timeData.nextSession?.date || exam.sessions[0]?.date || Date.now()).getFullYear(); if (timeData.nextSession && !timeData.timeRemaining.expired) { const { days, hours } = timeData.timeRemaining; const timeString = days > 0 ? `${days} Days ${hours}h Left` : `${hours}h Left`; const sessionInfo = exam.sessions.length > 1 ? ` (${timeData.nextSession.session})` : ''; - return `${exam.name} Countdown Timer - ${timeString}${sessionInfo} | ${exam.fullName} 2026`; + return `${exam.name} Countdown Timer - ${timeString}${sessionInfo} | ${exam.fullName} ${sessionYear}`; } - return `${exam.name} 2026 Countdown Timer | ${exam.fullName} Exam Date - TimeKeeper`; + return `${exam.name} ${sessionYear} Countdown Timer | ${exam.fullName} Exam Date - TimeKeeper`; } export function generateExamMetaDescription(exam: ExamData): string { const timeData = calculateTimeRemaining(exam); + const sessionYear = new Date(timeData.nextSession?.date || exam.sessions[0]?.date || Date.now()).getFullYear(); if (timeData.nextSession && !timeData.timeRemaining.expired) { const { days, hours, minutes } = timeData.timeRemaining; const timeString = `${days} days, ${hours} hours, ${minutes} minutes remaining`; const sessionInfo = exam.sessions.length > 1 ? ` for ${timeData.nextSession.session}` : ''; - return `${exam.fullName} countdown timer - ${timeString}${sessionInfo}. Track exact time until ${exam.name} 2026 exam. Real-time countdown with precision timing.`; + return `${exam.fullName} countdown timer - ${timeString}${sessionInfo}. Track exact time until ${exam.name} ${sessionYear} exam. Real-time countdown with precision timing.`; } return exam.metaDescription; From d49fcac44560b4ba07851dc5d82c9b9b60b9b000 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:30:23 +0000 Subject: [PATCH 3/5] Handle rollover edge cases for predicted sessions Agent-Logs-Url: https://github.com/kewonit/Timekeeper/sessions/3d6f935d-4fcf-498e-8c59-690661fda4a8 Co-authored-by: kewonit <108450560+kewonit@users.noreply.github.com> --- src/data/exam-data.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/data/exam-data.ts b/src/data/exam-data.ts index 85fd306..ebdb777 100644 --- a/src/data/exam-data.ts +++ b/src/data/exam-data.ts @@ -36,6 +36,7 @@ export interface ExamData extends ExamMetadata { let examDataCache: ExamData[] | null = null; let metadataLookup: Map | null = null; let sessionsLookup: Map | null = null; +const MAX_PREDICTION_YEARS = 5; /** * Initialize lookup maps for O(1) access @@ -67,11 +68,17 @@ function getSessionEndTime(session: ExamSession): number { function shiftISODate(date: string, years: number): string { const [year, month, day] = date.split('-').map(Number); - return new Date(Date.UTC(year + years, month - 1, day)).toISOString().split('T')[0]; + const shiftedDate = new Date(Date.UTC(year + years, month - 1, day)); + + if (shiftedDate.getUTCMonth() !== month - 1) { + shiftedDate.setUTCDate(0); + } + + return shiftedDate.toISOString().split('T')[0]; } function predictFutureSessions(sessions: ExamSession[], now: number): ExamSession[] { - for (let yearsToAdvance = 1; yearsToAdvance <= 5; yearsToAdvance++) { + for (let yearsToAdvance = 1; yearsToAdvance <= MAX_PREDICTION_YEARS; yearsToAdvance++) { const predictedSessions = sessions.map(session => ({ ...session, date: shiftISODate(session.date, yearsToAdvance), @@ -231,6 +238,8 @@ export function calculateTimeRemaining(exam: ExamData): { function calculateSingleTime(targetDate: string, now: number, endDate?: string) { const start = new Date(targetDate).getTime(); const end = new Date(endDate || targetDate).getTime(); + // Count down to the start date until the window begins, then count down to + // the end of the active exam window so ongoing sessions do not show expired. const target = now < start ? start : end; const distance = target - now; From bde48f2d62ee6a0330799ff317df8077451b8ec1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:31:40 +0000 Subject: [PATCH 4/5] Refine predicted session rollover safeguards Agent-Logs-Url: https://github.com/kewonit/Timekeeper/sessions/3d6f935d-4fcf-498e-8c59-690661fda4a8 Co-authored-by: kewonit <108450560+kewonit@users.noreply.github.com> --- src/data/exam-data.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/exam-data.ts b/src/data/exam-data.ts index ebdb777..322838f 100644 --- a/src/data/exam-data.ts +++ b/src/data/exam-data.ts @@ -70,6 +70,8 @@ function shiftISODate(date: string, years: number): string { const [year, month, day] = date.split('-').map(Number); const shiftedDate = new Date(Date.UTC(year + years, month - 1, day)); + // Clamp overflowed dates (for example, February 29 in a non-leap year) to + // the last valid day of the intended month instead of spilling into March. if (shiftedDate.getUTCMonth() !== month - 1) { shiftedDate.setUTCDate(0); } @@ -78,6 +80,8 @@ function shiftISODate(date: string, years: number): string { } function predictFutureSessions(sessions: ExamSession[], now: number): ExamSession[] { + let lastPredictedSessions = sessions; + for (let yearsToAdvance = 1; yearsToAdvance <= MAX_PREDICTION_YEARS; yearsToAdvance++) { const predictedSessions = sessions.map(session => ({ ...session, @@ -88,13 +92,14 @@ function predictFutureSessions(sessions: ExamSession[], now: number): ExamSessio ? `${session.note} (predicted next cycle from the latest published schedule)` : 'Predicted next cycle from the latest published schedule' })); + lastPredictedSessions = predictedSessions; if (predictedSessions.some(session => getSessionEndTime(session) >= now)) { return predictedSessions; } } - return sessions; + return lastPredictedSessions; } function normalizeSessionsForDisplay(sessions: ExamSession[]): ExamSession[] { From 7fc601c5eeb061c839a5588b5f82a5f28f90a963 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 06:32:50 +0000 Subject: [PATCH 5/5] Polish rollover documentation comments Agent-Logs-Url: https://github.com/kewonit/Timekeeper/sessions/3d6f935d-4fcf-498e-8c59-690661fda4a8 Co-authored-by: kewonit <108450560+kewonit@users.noreply.github.com> --- src/data/exam-data.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/exam-data.ts b/src/data/exam-data.ts index 322838f..9799617 100644 --- a/src/data/exam-data.ts +++ b/src/data/exam-data.ts @@ -70,8 +70,8 @@ function shiftISODate(date: string, years: number): string { const [year, month, day] = date.split('-').map(Number); const shiftedDate = new Date(Date.UTC(year + years, month - 1, day)); - // Clamp overflowed dates (for example, February 29 in a non-leap year) to - // the last valid day of the intended month instead of spilling into March. + // Clamp any overflowed date (for example, February 29 in a non-leap year) + // to the last valid day of the intended month instead of spilling over. if (shiftedDate.getUTCMonth() !== month - 1) { shiftedDate.setUTCDate(0); } @@ -244,7 +244,7 @@ function calculateSingleTime(targetDate: string, now: number, endDate?: string) const start = new Date(targetDate).getTime(); const end = new Date(endDate || targetDate).getTime(); // Count down to the start date until the window begins, then count down to - // the end of the active exam window so ongoing sessions do not show expired. + // the end of the active exam window so ongoing sessions are not shown as expired. const target = now < start ? start : end; const distance = target - now;