DEV Community

Cover image for Building CodeTune v1.2.0: Islamic Date Tracking & Audio Sync in a VS Code Extension
freerave
freerave

Posted on

Building CodeTune v1.2.0: Islamic Date Tracking & Audio Sync in a VS Code Extension

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.

CodeTune Main Dashboard

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Audio Player

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
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

💡 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>
Enter fullscreen mode Exit fullscreen mode
.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);
}
Enter fullscreen mode Exit fullscreen mode

Small detail, big impact — users now understand the feature instead of reporting it as a bug.


🐛 Other Notable Bug Fixes

Quran Reader

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 });
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Friday Surah Al-Kahf

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'))
]
Enter fullscreen mode Exit fullscreen mode

📁 Files Changed in v1.2.0

Update Screen

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)