How I implemented Fajr-based day tracking, fixed audio sync between webview and extension host, and modularized a VS Code Islamic productivity extension
I've been building CodeTune â a VS Code extension that helps Muslim developers stay spiritually connected while coding. It plays Quran recitations, shows Islamic reminders, tracks Dhikr, and calculates prayer times â all without leaving the editor.
Version 1.2.0 was a big release: 14 bug fixes, a complete modularization of the JavaScript layer, and most importantly â a proper Islamic day tracking system that respects Fajr as the start of the day, not midnight.
Here's a full technical breakdown of what changed and why.
đ The Core Challenge: Islamic Day Boundaries
In Islam, the new day begins at Maghrib (sunset) for lunar calendar events, and spiritually, many scholars consider Fajr (dawn) as the boundary for daily worship goals like Quran recitation and Dhikr.
This means if a user is doing Dhikr at 2:00 AM, they're still in yesterday's spiritual day â they haven't reached Fajr yet.
A naive implementation using midnight as the day boundary would:
- Reset their streak incorrectly at midnight
- Show "Day 0" progress when they're actively worshipping
- Break the Friday events logic (Surah Al-Kahf should start from Thursday Maghrib)
The Solution: getHabitTrackingDate()
private getHabitTrackingDate(date: Date = new Date()): string {
const now = new Date(date);
// Default to 4:00 AM if Fajr API hasn't responded yet
let fajrHour = 4;
let fajrMinute = 0;
if (this.currentFajrTime) {
fajrHour = this.currentFajrTime.getHours();
fajrMinute = this.currentFajrTime.getMinutes();
}
const fajrTimeToday = new Date(now);
fajrTimeToday.setHours(fajrHour, fajrMinute, 0, 0);
// If we're before Fajr, we're still in yesterday's Islamic day
if (now < fajrTimeToday) {
now.setDate(now.getDate() - 1);
}
return this.formatDate(now);
}
This single function is now the single source of truth for all date calculations in the tracker. No more getTodayString() scattered across the codebase.
Timezone-Safe Date Arithmetic
One subtle bug I fixed: calculating "yesterday" using new Date(dateString) can shift dates due to timezone offsets. The fix is to always use noon (12:00:00) as the time anchor:
private getPreviousDateString(baseDateStr: string, daysBack: number): string {
const [y, m, d] = baseDateStr.split('-').map(Number);
// Noon anchor prevents DST-related off-by-one errors
const date = new Date(y, m - 1, d, 12, 0, 0);
date.setDate(date.getDate() - daysBack);
return this.formatDate(date);
}
How Fajr Time Flows Through the System
The Fajr time comes from two sources and needs to reach SpiritualTracker:
Source 1 â IP Geolocation on startup (ActivityBarViewProvider.ts):
case 'requestPrayerTimes':
const times = calculatePrayerTimes(data.lat, data.lon);
this.sendMessageToWebview({ type: 'receivePrayerTimes', payload: times });
// Update SpiritualTracker with real Fajr time
const tracker = (global as any).spiritualTracker as SpiritualTracker;
if (tracker && times.fajr) {
tracker.updateFajrTime(new Date(times.fajr));
}
break;
Source 2 â IslamicRemindersManager on init (IslamicRemindersManager.ts):
private syncFajrTimeWithTracker() {
if (!this.spiritualTracker) return;
try {
const prayerTimes = IslamicCalendar.calculatePrayerTimes();
if (prayerTimes?.fajr) {
this.spiritualTracker.updateFajrTime(prayerTimes.fajr);
}
} catch (error) {
logger.warn('Could not sync Fajr time with Tracker:', error);
}
}
The result: as soon as the extension loads, SpiritualTracker gets a Fajr time from the local calculation, and once the GPS/IP location resolves, it gets updated to the precise value.
đĩ Fixing the Audio â Tracker Sync Bug
This was the most impactful bug in the release. The Spiritual Progress Dashboard was always showing "0/15 min" for Quran listening, even after hours of audio playback.
Root Cause
The data flow was broken at step 2:
â
Audio plays â saves time to localStorage (webview)
â MISSING: localStorage data never sent to extension host
â SpiritualTracker stays at 0 minutes
â Dashboard reads from SpiritualTracker â shows 0
The webview and extension host are separate processes. localStorage in the webview is invisible to the TypeScript extension code.
The Fix: The Accumulator Pattern (Time Buffer)
A naive fix would be:
// â Loses short sessions (45 seconds rounds to 0)
const elapsedMinutes = Math.round(elapsedTime / 60000);
if (elapsedMinutes > 0) {
this.postMessage('logQuranTime', { minutes: elapsedMinutes });
}
The problem: a user who listens 3 times for 45 seconds each gets 0 minutes logged, even though they listened for over 2 minutes total.
The better approach â accumulate milliseconds until a full minute is ready:
// In constructor
this.unloggedMs = 0;
stopTimeTracking() {
if (!this.playStartTime) return;
const now = Date.now();
const elapsedMs = now - this.playStartTime;
this.currentSessionTime += elapsedMs;
// Add to time buffer
this.unloggedMs += elapsedMs;
// localStorage tracking (existing)
const today = new Date().toISOString().split('T')[0];
if (!this.listeningStats.dailyTimeStats[today]) {
this.listeningStats.dailyTimeStats[today] = 0;
}
this.listeningStats.totalTimePlayed += elapsedMs;
this.listeningStats.dailyTimeStats[today] += elapsedMs;
this.listeningStats.lastUpdated = now;
this.saveListeningStats();
// Only send complete minutes to SpiritualTracker
const minutesToLog = Math.floor(this.unloggedMs / 60000);
if (minutesToLog > 0) {
this.postMessage('logQuranTime', { minutes: minutesToLog });
// Keep the remainder for next session
this.unloggedMs -= (minutesToLog * 60000);
}
this.playStartTime = null;
this.currentSessionTime = 0;
}
Why this is correct:
| Scenario | Math.round (broken) | Accumulator (fixed) |
|---|---|---|
| 3 Ã 45-second sessions | 0 min logged â | ~2.25 min logged â |
| 1 Ã 90-second session | 2 min logged (wrong) â | 1 min logged, 30s saved â |
| 1 Ã 5-minute session | 5 min â | 5 min â |
The accumulator pattern ensures no listening time is lost AND no phantom minutes are added.
Streak Calculation
The streak logic needed to handle the Fajr boundary too. A user who hasn't done Dhikr yet today (it's 10 AM) shouldn't lose their streak â they still have until the next Fajr.
private calculateCurrentStreak(activeDates: string[]): number {
if (activeDates.length === 0) return 0;
const activeSet = new Set(activeDates);
const todayStr = this.getHabitTrackingDate();
const yesterdayStr = this.getPreviousDateString(todayStr, 1);
// Streak survives if either today OR yesterday has activity
if (!activeSet.has(todayStr) && !activeSet.has(yesterdayStr)) {
return 0;
}
let streak = 0;
let currentCheckStr = activeSet.has(todayStr) ? todayStr : yesterdayStr;
for (let i = 0; i < 365; i++) {
if (activeSet.has(currentCheckStr)) {
streak++;
currentCheckStr = this.getPreviousDateString(currentCheckStr, 1);
} else {
break;
}
}
return streak;
}
đĄ UX: Explaining Islamic Tracking to Users
One challenge: users might think the tracker is broken when their daily goals don't reset at midnight. The solution â a small info tooltip next to the dashboard title:
<div class="info-tooltip-container">
<span class="info-icon">âšī¸</span>
<div class="info-tooltip-content">
<strong>đ Smart Islamic Tracking</strong><br>
Daily goals reset at <b>Fajr</b> (not midnight).<br>
Friday events begin after <b>Maghrib</b> the evening before.
</div>
</div>
.info-tooltip-content {
visibility: hidden;
opacity: 0;
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%) translateY(10px);
width: 220px;
background: var(--glass-bg, rgba(30, 30, 30, 0.95));
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border, rgba(255, 255, 255, 0.1));
border-radius: 8px;
padding: 12px;
font-size: 12px;
transition: all 0.3s ease;
pointer-events: none;
}
.info-tooltip-container:hover .info-tooltip-content {
visibility: visible;
opacity: 1;
transform: translateX(-50%) translateY(0);
}
Small detail, big impact â users now understand the feature instead of reporting it as a bug.
đ Other Notable Bug Fixes
Dhikr counters not updating the dashboard â The counter component was incrementing its own local state but never notifying the extension host:
// Before: only updated local UI
function incrementSalawatCounter() {
localCount++;
updateDisplay();
}
// After: also syncs to SpiritualTracker
function incrementSalawatCounter() {
localCount++;
updateDisplay();
window.vscode?.postMessage({ type: 'incrementDhikr', count: 1 });
}
Adhkar hyphenated names not resolving â rabbi-ghifir was being looked up as-is instead of converting to rabbiGhifir:
function incrementAdhkarCounter(adhkarName) {
const camelCaseName = adhkarName.replace(
/-([a-z])/g,
(g) => g[1].toUpperCase()
);
// Now correctly resolves rabbi-ghifir â rabbiGhifir
}
Missing configuration properties â VS Code showed "Unable to write to User Settings" because fridaySurahLastReset and fridaySurahState were never registered in package.json:
{
"contributes": {
"configuration": {
"properties": {
"codeTune.fridaySurahLastReset": {
"type": "string",
"default": ""
},
"codeTune.fridaySurahState": {
"type": "string",
"default": "disabled"
}
}
}
}
}
Webview can't load new_features.txt â Getting 403 Forbidden because src/media wasn't in localResourceRoots:
localResourceRoots: [
vscode.Uri.file(path.join(context.extensionPath, 'src', 'media')),
vscode.Uri.file(path.join(context.extensionPath, 'images'))
]
đ Files Changed in v1.2.0
| File | Change |
|---|---|
src/utils/SpiritualTracker.ts |
Islamic date logic, Fajr-based tracking |
src/logic/activityBarView.ts |
Prayer times sync, Fajr update on location |
src/logic/islamicReminders.ts |
SpiritualTracker integration |
src/ui/components/audioPlayer.js |
Accumulator audio sync fix |
src/ui/components/counter.js |
Dhikr â SpiritualTracker sync |
src/ui/components/trackerDashboard.js |
Info tooltip |
src/ui/components/trackerDashboard.css |
Card wrapper, modal fixes |
package.json |
Missing configuration properties |
Deleted: src/ui/components/statistics.js (duplicate of dashboard)
What's Next
v1.3.0 will focus on Settings & Localization â fixing all settings panels, ensuring every string is properly localized across all 5 languages (EN, AR, ES, FR, RU), and unifying the settings UI across all components.
If you're building VS Code extensions with webviews, the key lesson from this release: the webview and extension host are separate processes. Any state that needs to persist or be shared must explicitly cross the message bridge. localStorage is a webview-only sandbox.
The extension is open source â feedback and contributions are welcome.
Ø¨Ø§ØąŲ Ø§ŲŲŲ ŲŲŲŲ đ¤˛
đ Links
| đĻ VS Code Marketplace | Install CodeTune |
| đŖ Open VSX Registry | Install CodeTune |
| đ GitHub | kareem2099/codetune |





Top comments (0)