Portal App
Classical Guitar Practice Hub
://drjclassicalguitar.com
Today
0m
Week Total
0m
Streak
0d 🔥
Pro Metronome
Increases rhythm speed automatically by +2 BPM every 30 seconds to optimize velocity adaptation without microtension.
Performance Deadlines
Target Composition / Event Target Date Add Milestone Saved Practice Workflows ☕ Daily Warm-Up (20 mins) Giuliani Arpeggios & Segovia Linear Scales 🏛️ Repertoire Polish (45 mins) Old Repertoire Maintenance & Phrasing Stamina 🎓 Deep Practice Cycle (90 mins) Isolated Friction Points & Speed Escalation cycles Active Practice Tracker Pedagogical Target Focus Techniques (Arpeggios, Slurs, Tremolo) Scales (Segovia, Diatonic patterns, Speed) New Repertoire (Sight-reading, Memorization) Old Repertoire (Polishing, Memory Refresh) Sight Reading & Applied Analysis 00:00:00 Start Reset Or Overwrite Duration Manually Max Structural Clean Tempo (BPM) Microjournaling (Breakthroughs or Friction measures) Archive Session to Log Ledger Technique Checklist Reset All Historian Charts Day-by-Day Focus Density (Mins) Pedagogical Split (Balance Check) History Ledger CSV Email Dr. J Studio Destination: Date Focus Vol BPM Reset Profile Ledger // System Data State Context let timerInterval = null; let elapsedSeconds = 0; let isRunning = false; let practiceData = []; let milestones = []; // Audio Metronome Subsystem Context Thread let audioCtx = null; let metroInterval = null; let isMetroPlaying = false; let currentBeat = 0; let nextClickTime = 0.0; const lookahead = 25.0; const scheduleAheadTime = 0.1; // Speed Trainer Automation Context Thread let speedTrainerInterval = null; let isSpeedTrainerActive = false; // Chart Registry Instantiations let dailyChartInst = null; let categoryChartInst = null; // Life Cycle Bootloader Hook window.addEventListener('DOMContentLoaded', () => { // Synchronize Icon Vectors lucide.createIcons(); // Load Practice Data Ledger Cache const savedData = localStorage.getItem('drj_guitar_practice_logs'); if (savedData) { practiceData = JSON.parse(savedData); } else { practiceData = [ { date: '2026-10-25', type: 'Techniques', minutes: 20, bpm: 84, notes: 'Giuliani Right Hand variations' }, { date: '2026-10-26', type: 'Scales', minutes: 15, bpm: 92, notes: 'Segovia C major' }, { date: '2026-10-27', type: 'New Repertoire', minutes: 45, bpm: 60, notes: 'Learning Bach Chaconne' }, { date: '2026-10-28', type: 'Old Repertoire', minutes: 30, bpm: 110, notes: 'Villa-Lobos Prelude 1' }, { date: '2026-10-30', type: 'Techniques', minutes: 25, bpm: 90, notes: 'Left hand independence' } ]; localStorage.setItem('drj_guitar_practice_logs', JSON.stringify(practiceData)); } // Sync configured target configuration address const savedEmail = localStorage.getItem('drj_teacher_email'); if (savedEmail) { document.getElementById('teacherEmail').value = savedEmail; } // Synchronize Milestone Timelines const savedMilestones = localStorage.getItem('drj_guitar_milestones'); if (savedMilestones) { milestones = JSON.parse(savedMilestones); } else { milestones = [ { id: 1, title: "Spring Recital Assessment Run", date: "2027-04-15" }, { id: 2, title: "Studio Performance Masterclass Critique", date: "2026-12-05" } ]; localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); } initCharts(); updateUI(); renderMilestones(); updateVisualIndicators(); }); // Event hooks monitoring config target addresses document.getElementById('teacherEmail').addEventListener('change', (e) => { localStorage.setItem('drj_teacher_email', e.target.value); }); // CSV DATA SERIALIZATION FRAMEWORK function downloadCSV() { if (practiceData.length === 0) { alert("The ledger registry profile is empty. Log a session to build a profile record."); return; } let csvRows = ["Date,Category,Duration_Minutes,Metronome_BPM,Session_Progress_Notes"]; practiceData.forEach(item => { let quoteCleanedNotes = item.notes.replace(/"/g, '""'); csvRows.push("${item.date}","${item.type}",${item.minutes},"${item.bpm}","${quoteCleanedNotes}"); }); const blob = new Blob([csvRows.join("\n")], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const hiddenAnchor = document.createElement("a"); hiddenAnchor.setAttribute("href", url); hiddenAnchor.setAttribute("download", "DrJ_Studio_Guitar_Practice_History.csv"); document.body.appendChild(hiddenAnchor); hiddenAnchor.click(); document.body.removeChild(hiddenAnchor); } // MAIL DISTRIBUTION HYPERLINK FORMATTER function emailHistory() { if (practiceData.length === 0) { alert("The ledger configuration registry history is empty."); return; } const activeDestination = document.getElementById('teacherEmail').value || 'lessons@drjclassicalguitar.com'; const subjectHeader = encodeURIComponent("Classical Guitar Studio Practice Log Update"); let messageContext = "Hello Dr. J,\n\nHere is my recent tactical guitar practice session updates summary submitted via the studio dashboard module:\n\n"; messageContext += "========================================================\n"; messageContext += "DATE | TYPE METRIC | DURATION | TARGET CEP | NOTES\n"; messageContext += "========================================================\n"; const reverseTruncatedLogs = [...practiceData].reverse().slice(0, 15); reverseTruncatedLogs.forEach(i => { let padType = i.type.padEnd(14, ' '); let padVol = (i.minutes + "m").padEnd(8, ' '); let padBpm = (i.bpm + " bpm").padEnd(10, ' '); messageContext += ${i.date} | ${padType} | ${padVol} | ${padBpm} | ${i.notes}\n; }); if (practiceData.length > 15) { messageContext += \n...and ${practiceData.length - 15} additional historic sequence tracks archived on device caching layers.; } messageContext += "\n========================================================\n\nSent from the Guitar Practice Suite App."; window.location.href = mailto:${activeDestination}?subject=${subjectHeader}&body=${encodeURIComponent(messageContext)}; } // AUDIO CONTEXT CORE CLOCK MODULES function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } } function adjustBpm(delta) { const el = document.getElementById('metroBpm'); let updated = parseInt(el.value) + delta; updated = Math.max(40, Math.min(250, updated)); el.value = updated; updateMetroTempo(); } function updateMetroTempo() { document.getElementById('metronomeBpm').value = document.getElementById('metroBpm').value; } function toggleSpeedTrainerMode() { const check = document.getElementById('speedTrainerToggle'); const status = document.getElementById('trainerStatus'); isSpeedTrainerActive = check.checked; if (isSpeedTrainerActive) { status.innerText = 'Trainer Armed'; status.className = "text-[11px] font-semibold tracking-wide uppercase px-2 py-0.5 rounded-full bg-amber-50 text-amber-700 border border-amber-200 animate-pulse"; } else { status.innerText = 'Manual Mode'; status.className = "text-[11px] font-medium tracking-wide uppercase px-2 py-0.5 rounded-full bg-stone-100 text-stone-500 border border-stone-200"; clearInterval(speedTrainerInterval); } } function startSpeedTrainerTimer() { if (!isSpeedTrainerActive) return; document.getElementById('trainerStatus').innerText = 'Trainer Live 🚀'; speedTrainerInterval = setInterval(() => { const bpmBox = document.getElementById('metroBpm'); let cur = parseInt(bpmBox.value); if (cur < 250) { bpmBox.value = cur + 2; updateMetroTempo(); } }, 30000); } function stopSpeedTrainerTimer() { clearInterval(speedTrainerInterval); toggleSpeedTrainerMode(); } function scheduler() { while (nextClickTime < audioCtx.currentTime + scheduleAheadTime) { scheduleClick(currentBeat, nextClickTime); advanceBeat(); } } function scheduleClick(beatNumber, time) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); // Frequency assignments contrasting metric subdivisions osc.frequency.setValueAtTime(beatNumber === 0 ? 987.77 : 523.25, time); gain.gain.setValueAtTime(0.6, time); gain.gain.exponentialRampToValueAtTime(0.001, time + 0.04); osc.start(time); osc.stop(time + 0.04); const totalBeats = parseInt(document.getElementById('metroBeats').value); setTimeout(() => { triggerVisualFlash(beatNumber, totalBeats); }, (time - audioCtx.currentTime) * 1000); } function advanceBeat() { const speedMetric = parseInt(document.getElementById('metroBpm').value); nextClickTime += (60.0 / speedMetric); currentBeat = (currentBeat + 1) % parseInt(document.getElementById('metroBeats').value); } function toggleMetronome() { initAudio(); const btn = document.getElementById('metroPlayBtn'); if (!isMetroPlaying) { if (audioCtx.state === 'suspended') { audioCtx.resume(); } isMetroPlaying = true; currentBeat = 0; nextClickTime = audioCtx.currentTime + 0.02; metroInterval = setInterval(scheduler, lookahead); btn.innerHTML = Stop Metronome; btn.className = "w-full bg-gradient-to-r from-red-700 to-red-800 hover:from-red-600 hover:to-red-700 text-white font-semibold p-3.5 rounded-2xl transition duration-300 shadow-md tracking-wide text-sm flex items-center justify-center gap-2"; lucide.createIcons(); startSpeedTrainerTimer(); } else { isMetroPlaying = false; clearInterval(metroInterval); btn.innerHTML = Start Metronome; btn.className = "w-full bg-gradient-to-r from-amber-800 to-amber-900 hover:from-amber-700 hover:to-amber-800 text-white font-semibold p-3.5 rounded-2xl transition duration-300 shadow-md tracking-wide text-sm flex items-center justify-center gap-2"; lucide.createIcons(); clearFlashIndicators(); stopSpeedTrainerTimer(); } } function updateVisualIndicators() { const limit = parseInt(document.getElementById('metroBeats').value); for (let i = 1; i <= 4; i++) { document.getElementById(metroFlash${i}).style.display = (i <= limit) ? 'block' : 'none'; } } document.getElementById('metroBeats').addEventListener('change', updateVisualIndicators); function triggerVisualFlash(beatIndex, total) { clearFlashIndicators(); const node = document.getElementById(metroFlash${beatIndex + 1}); if (node) { if (beatIndex === 0) { node.className = "w-4 h-4 rounded-full bg-amber-500 shadow-md shadow-amber-500/50 border border-amber-400 scale-125 transition-all duration-75"; } else { node.className = "w-4 h-4 rounded-full bg-stone-700 shadow-sm border border-stone-600 scale-110 transition-all duration-75"; } } } function clearFlashIndicators() { for (let i = 1; i <= 4; i++) { document.getElementById(metroFlash${i}).className = "w-4 h-4 rounded-full bg-stone-300 border border-stone-200 shadow-inner transition-all duration-200"; } } // PERFORMANCE MATRIX DATA PARSERS function addMilestone() { const tBox = document.getElementById('eventTitle'); const dBox = document.getElementById('eventDate'); if (!tBox.value || !dBox.value) { alert("Please add composition description tags and target dates."); return; } milestones.push({ id: Date.now(), title: tBox.value, date: dBox.value }); localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); tBox.value = ''; dBox.value = ''; renderMilestones(); } function deleteMilestone(id) { milestones = milestones.filter(m => m.id !== id); localStorage.setItem('drj_guitar_milestones', JSON.stringify(milestones)); renderMilestones(); } function renderMilestones() { const root = document.getElementById('milestoneContainer'); root.innerHTML = ''; if (milestones.length === 0) { root.innerHTML =No active target schedules configured.
; return; } const sorted = [...milestones].sort((a,b) => new Date(a.date) - new Date(b.date)); sorted.forEach(m => { const targetTime = new Date(m.date + 'T00:00:00'); const today = new Date(); today.setHours(0,0,0,0); const remainderDays = Math.ceil((targetTime.getTime() - today.getTime()) / (1000 * 3600 * 24)); let badge = ''; if (remainderDays < 0) { badge = Archive; } else if (remainderDays <= 14) { badge = ${remainderDays} days out; } else { badge = ${remainderDays} days left; } const div = document.createElement('div'); div.className = "py-2.5 flex items-center justify-between gap-2 first:pt-0"; div.innerHTML = ${m.title} ${m.date}
${badge}
;
root.appendChild(div);
});
lucide.createIcons();
}
// CHRONOMETER ENGINE CONTROL SETS
function toggleTimer() {
const btn = document.getElementById('startBtn');
if (!isRunning) {
isRunning = true;
btn.innerHTML = Pause;
btn.className = "px-5 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1";
timerInterval = setInterval(() => { elapsedSeconds++; updateTimerDisplay(); }, 1000);
} else {
isRunning = false;
btn.innerHTML = Start;
btn.className = "px-5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1";
clearInterval(timerInterval);
}
lucide.createIcons();
}
function resetTimer() {
isRunning = false; clearInterval(timerInterval); elapsedSeconds = 0; updateTimerDisplay();
const btn = document.getElementById('startBtn');
btn.innerHTML = Start;
btn.className = "px-5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-semibold shadow-sm transition duration-300 flex items-center gap-1";
lucide.createIcons();
}
function updateTimerDisplay() {
const hrs = String(Math.floor(elapsedSeconds / 3600)).padStart(2, '0');
const mins = String(Math.floor((elapsedSeconds % 3600) / 60)).padStart(2, '0');
const secs = String(elapsedSeconds % 60).padStart(2, '0');
document.getElementById('timerDisplay').innerText = ${hrs}:${mins}:${secs};
}
function loadRoutine(routineKey) {
const typeSelect = document.getElementById('practiceType');
const notesArea = document.getElementById('practiceNotes');
const manualIn = document.getElementById('manualMinutes');
if (routineKey === 'warmup') {
typeSelect.value = 'Techniques'; manualIn.value = 20;
notesArea.value = '[Daily Warm-Up] Microalignment check: focus entirely on stroke follow-through relaxation.';
} else if (routineKey === 'repertoire') {
typeSelect.value = 'Old Repertoire'; manualIn.value = 45;
notesArea.value = '[Repertoire Maintenance] Structural evaluation: check dynamic contour stability over complete performance cycles.';
} else if (routineKey === 'complete') {
typeSelect.value = 'New Repertoire'; manualIn.value = 90;
notesArea.value = '[Deep Practice Cycle] Analytical isolated breakdown targeting localized synchronization thresholds.';
}
}
function saveSession() {
let loggedMinutes = 0;
const manualVal = parseInt(document.getElementById('manualMinutes').value);
if (manualVal && manualVal > 0) {
loggedMinutes = manualVal;
} else if (elapsedSeconds > 0) {
loggedMinutes = Math.max(1, Math.round(elapsedSeconds / 60));
} else {
alert("Chronometer tracking data is empty. Initialize timer loops before logging.");
return;
}
const activeLog = {
date: new Date().toISOString().split('T')[0],
type: document.getElementById('practiceType').value,
minutes: loggedMinutes,
bpm: parseInt(document.getElementById('metronomeBpm').value) || '-',
notes: document.getElementById('practiceNotes').value || 'Session logging complete.'
};
practiceData.push(activeLog);
localStorage.setItem('drj_guitar_practice_logs', JSON.stringify(practiceData));
resetTimer();
document.getElementById('manualMinutes').value = '';
document.getElementById('metronomeBpm').value = '';
document.getElementById('practiceNotes').value = '';
updateUI();
updateCharts();
}
function clearLogs() {
if (confirm("This action permanently flushes storage configuration tracking memory. Proceed?")) {
practiceData = [];
localStorage.removeItem('drj_guitar_practice_logs');
updateUI();
updateCharts();
}
}
function resetChecklist() {
document.querySelectorAll('.tech-check').forEach(box => box.checked = false);
}
function updateUI() {
const rightNow = new Date().toISOString().split('T')[0];
let rawToday = 0; let rawWeek = 0;
const thresholdWeek = new Date(); thresholdWeek.setDate(thresholdWeek.getDate() - 7);
practiceData.forEach(i => {
if (i.date === rightNow) rawToday += i.minutes;
if (new Date(i.date) >= thresholdWeek) rawWeek += i.minutes;
});
document.getElementById('todayTotal').innerText = ${rawToday}m;
document.getElementById('weekTotal').innerText = ${rawWeek}m;
document.getElementById('streakCount').innerHTML = ${[...new Set(practiceData.map(i => i.date))].length}d 🔥;
const tbody = document.getElementById('ledgerBody');
tbody.innerHTML = '';
[...practiceData].reverse().forEach(item => {
const tr = document.createElement('tr');
tr.className = "hover:bg-stone-50 transition border-b border-stone-100 last:border-0";
tr.innerHTML = `
${item.date.slice(5)}
${item.type.replace(' Repertoire','')}
${item.minutes}m
${item.bpm}
`;
tbody.appendChild(tr);
});
}
function initCharts() {
const ctxDaily = document.getElementById('dailyChart').getContext('2d');
dailyChartInst = new Chart(ctxDaily, {
type: 'line',
data: { labels: [], datasets: [{ label: 'Minutes', data: [], borderColor: '#78350f', borderWidth: 2.5, pointBackgroundColor: '#78350f', backgroundColor: 'rgba(120, 53, 15, 0.04)', fill: true, tension: 0.35 }] },
options: { responsive: true, maintainAspectRatio: false, scales: { x: { grid: { display: false } }, y: { grid: { color: '#f3f0ea' }, border: { dash: [5, 5] }, min: 0 } }, plugins: { legend: { display: false } } }
});
const ctxCat = document.getElementById('categoryChart').getContext('2d');
categoryChartInst = new Chart(ctxCat, {
type: 'doughnut',
data: {
labels: ['Techniques', 'Scales', 'New Repertoire', 'Old Repertoire', 'Sight Reading'],
datasets: [{ data: [], backgroundColor: ['#78350f', '#b45309', '#d97706', '#f59e0b', '#a8a29e'], borderStrokeWidth: 0, hoverOffset: 4 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, cutout: '75%' }
});
updateCharts();
}
function updateCharts() {
if (!dailyChartInst || !categoryChartInst) return;
const chronologicalTail = [...practiceData].sort((a,b) => new Date(a.date) - new Date(b.date)).slice(-7);
dailyChartInst.data.labels = chronologicalTail.map(l => l.date.slice(5));
dailyChartInst.data.datasets[0].data = chronologicalTail.map(l => l.minutes);
dailyChartInst.update();
const matrix = { 'Techniques': 0, 'Scales': 0, 'New Repertoire': 0, 'Old Repertoire': 0, 'Sight Reading': 0 };
practiceData.forEach(item => { if (matrix[item.type] !== undefined) matrix[item.type] += item.minutes; });
categoryChartInst.data.datasets[0].data = [matrix['Techniques'], matrix['Scales'], matrix['New Repertoire'], matrix['Old Repertoire'], matrix['Sight Reading']];
categoryChartInst.update();
}

