// Logic ported from parox.py const DATE_DEBUT = new Date(2026, 0, 5); // Month is 0-indexed in JS (Jan = 0) const paliers = [ { nom: "≈17.5 mg (20/20/20/10)", pattern: [10, 20, 20, 20], jours: 7, moyenne: 17.5 }, { nom: "≈16.7 mg (20/20/10)", pattern: [10, 20, 20], jours: 7, moyenne: 16.7 }, { nom: "≈15 mg (20/10)", pattern: [10, 20], jours: 7, moyenne: 15 }, { nom: "≈13.3 mg (20/10/10)", pattern: [10, 10, 20], jours: 7, moyenne: 13.3 }, { nom: "10 mg (stabilisation)", pattern: [10], jours: 14, moyenne: 10 }, { nom: "≈7.5 mg (10/10/10/0)", pattern: [10, 0, 10, 10], jours: 7, moyenne: 7.5 }, { nom: "≈6.7 mg (10/10/0)", pattern: [10, 0, 10], jours: 7, moyenne: 6.7 }, { nom: "≈5 mg (10/0)", pattern: [10, 0], jours: 7, moyenne: 5 }, { nom: "≈3.3 mg (10/0/0)", pattern: [10, 0, 0], jours: 7, moyenne: 3.3 }, { nom: "Arrêt complet", pattern: [0], jours: 7, moyenne: 0 } ]; let state = { ressentis: [], todayDose: 0, selectedRating: null, selectedEmoji: null, authToken: null }; // Utils: Date comparison function isSameDay(d1, d2) { return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate(); } function formatDateISO(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } // Initial Load document.addEventListener('DOMContentLoaded', () => { document.getElementById('current-date').textContent = new Date().toLocaleDateString(); // Check local storage for session const savedToken = localStorage.getItem('parox_token'); if (savedToken) { // Optimistically try to unlock attemptLogin(savedToken); } // Bind Enter key on password document.getElementById('password-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') attemptLogin(); }); }); async function attemptLogin(tokenOverride = null) { const pwdInput = document.getElementById('password-input'); const password = tokenOverride || pwdInput.value; const errorMsg = document.getElementById('login-error'); errorMsg.textContent = "Verifying..."; try { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }) }); const data = await res.json(); if (data.success) { state.authToken = data.token; localStorage.setItem('parox_token', data.token); // Unlock UI document.getElementById('login-overlay').style.display = 'none'; document.getElementById('app-container').classList.remove('blur-hidden'); loadData(); } else { errorMsg.textContent = "ACCESS DENIED"; pwdInput.value = ""; localStorage.removeItem('parox_token'); } } catch (e) { errorMsg.textContent = "CONNECTION ERROR"; } } function logout() { localStorage.removeItem('parox_token'); location.reload(); } function setStatus(msg) { const time = new Date().toLocaleTimeString('fr-FR'); const el = document.getElementById('system-msg'); if (el) el.textContent = `[${time}] ${msg}`; } async function loadData() { setStatus("Fetching Data..."); try { const res = await fetch('/api/data', { headers: { 'x-auth-token': state.authToken } }); if (res.status === 401) return logout(); const data = await res.json(); state.ressentis = data; setStatus("Data Loaded."); initApp(); } catch (e) { setStatus("Fetch Error: " + e.message); } } const MOIS = ["Janv.", "Févr.", "Mars", "Avr.", "Mai", "Juin", "Juil.", "Août", "Sept.", "Oct.", "Nov.", "Déc."]; const JOURS_SEM = ["lun.", "mar.", "mer.", "jeu.", "ven.", "sam.", "dim."]; function initApp() { renderFullPlan(); renderCalendar(); renderInputStatus(); } function renderFullPlan() { const today = new Date(); today.setHours(0, 0, 0, 0); let html = ""; let runningDate = new Date(DATE_DEBUT); paliers.forEach(palier => { // Wrapper for Palier html += `
`; html += `
=== ${palier.nom} ===
`; html += `
Pattern : [${palier.pattern.join(', ')}] mg | Moyenne ≈ ${palier.moyenne} mg | Durée : ${palier.jours} jours
`; let daysHtml = ""; let futureBuffer = []; let currentMonth = -1; for (let j = 0; j < palier.jours; j++) { let dose = palier.pattern[j % palier.pattern.length]; let dateStr = `${JOURS_SEM[runningDate.getDay() === 0 ? 6 : runningDate.getDay() - 1]} ${runningDate.getDate()} ${MOIS[runningDate.getMonth()]}`; let isoDate = formatDateISO(runningDate); // Check if past/today or future if (runningDate <= today) { // Render full log line let entry = state.ressentis.find(r => r.date === isoDate); let lineClass = (runningDate.getTime() === today.getTime()) ? "today-line" : ""; let uniqueId = `comment-${isoDate}`; daysHtml += `
${dateStr}: ${dose} mg`; if (entry) { let stars = "★".repeat(entry.rating || 0); let comment = entry.commentaire || ''; let needsToggle = comment.length > 30; // Threshold for showing view more daysHtml += ` ${stars} ${entry.emoji || ''}`; if (comment) { daysHtml += ` `; daysHtml += `${comment}`; if (needsToggle) { daysHtml += ` +`; } daysHtml += ``; } } if (runningDate.getTime() === today.getTime()) { const timeStr = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); daysHtml += ` (aujourd'hui, ${timeStr})`; } daysHtml += `
`; } else { // It is future, group by month like python script if (currentMonth !== runningDate.getMonth()) { if (futureBuffer.length > 0) { daysHtml += `
Jours à venir ${MOIS[currentMonth]} ${runningDate.getFullYear()} : ${futureBuffer.join(', ')}
`; futureBuffer = []; } currentMonth = runningDate.getMonth(); } futureBuffer.push(`${runningDate.getDate()}(${dose}mg)`); } // Increment date runningDate.setDate(runningDate.getDate() + 1); } // Flush buffer if (futureBuffer.length > 0) { // Use prev date to get correct year/month for the buffer let prev = new Date(runningDate); prev.setDate(prev.getDate() - 1); daysHtml += `
Jours à venir ${MOIS[prev.getMonth()]} ${prev.getFullYear()} : ${futureBuffer.join(', ')}
`; } html += daysHtml + `
`; // Close palier-block }); document.getElementById('schedule-output').innerHTML = html; } function renderCalendar() { // Generate multi-month grid let html = `
Légende : =20mg =10mg _=0mg
`; let currentDate = new Date(DATE_DEBUT); // Find end date let totalDays = paliers.reduce((acc, p) => acc + p.jours, 0); let endDate = new Date(DATE_DEBUT); endDate.setDate(endDate.getDate() + totalDays); // We need to group by month // Iterating day by day to get dose let monthGrid = []; // Array of {monthName, weeks: [ [ {day, dose} ] ]} let runningDate = new Date(DATE_DEBUT); let palierIndex = 0; let dayInPalier = 0; let currentBlock = { month: "", weeks: [] }; let currentWeek = Array(7).fill(null); // Helper to get dose at absolute index let absIndex = 0; while (runningDate < endDate) { // Get Dose let curPalier = paliers[palierIndex]; let dose = curPalier.pattern[dayInPalier % curPalier.pattern.length]; let jsMonth = runningDate.getMonth(); let monthName = MOIS[jsMonth]; // Start new month block if needed if (currentBlock.month !== monthName) { if (currentBlock.month !== "") { // Push old month if (currentWeek.some(d => d !== null)) currentBlock.weeks.push(currentWeek); monthGrid.push(currentBlock); } currentBlock = { month: monthName, weeks: [] }; currentWeek = Array(7).fill(null); // Fill offset let dayOfWeek = (runningDate.getDay() + 6) % 7; // Mon=0 // No, standard JS getDay: Sun=0, Mon=1... // Python script output shows L M M J V S D. So Mon is first column. // (day + 6) % 7 converts Sun(0)->6, Mon(1)->0. Correct. } let colIndex = (runningDate.getDay() + 6) % 7; if (colIndex === 0 && currentWeek.some(d => d !== null)) { // New week row currentBlock.weeks.push(currentWeek); currentWeek = Array(7).fill(null); } // Add day currentWeek[colIndex] = { day: runningDate.getDate(), dose: dose, isToday: isSameDay(runningDate, new Date()) }; // Advance runningDate.setDate(runningDate.getDate() + 1); dayInPalier++; if (dayInPalier >= curPalier.jours) { palierIndex++; dayInPalier = 0; if (palierIndex >= paliers.length) break; } } // Flush last if (currentWeek.some(d => d !== null)) currentBlock.weeks.push(currentWeek); if (currentBlock.weeks.length > 0) monthGrid.push(currentBlock); // Render HTML monthGrid.forEach(m => { html += `
`; html += `
--- ${m.month} ---
`; html += `
LMMJVSD
`; m.weeks.forEach(week => { html += `
`; for (let i = 0; i < 7; i++) { let cell = week[i]; if (cell) { let symbol = "_"; let dClass = "dose-0"; if (cell.dose === 20) { symbol = "█"; dClass = "dose-20"; } if (cell.dose === 10) { symbol = "▒"; dClass = "dose-10"; } let dayClass = cell.isToday ? "day-cell today-h" : "day-cell"; html += `${cell.day.toString().padStart(2, '0')}${symbol}`; } else { html += ``; } } html += `
`; }); html += `
`; }); document.getElementById('calendar-output').innerHTML = html; } function getDayInfo(targetDate) { let diffTime = targetDate.getTime() - DATE_DEBUT.getTime(); let diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); if (diffDays < 0) return { dose: '?', palier: 'Before Start' }; let runningDay = 0; for (let p of paliers) { if (diffDays < runningDay + p.jours) { let indexInPalier = diffDays - runningDay; let dose = p.pattern[indexInPalier % p.pattern.length]; return { dose: dose, palier: p.nom }; } runningDay += p.jours; } return { dose: 0, palier: "Finished" }; } function renderInputStatus() { const now = new Date(); const isLateEnough = now.getHours() >= 18; const todayStr = formatDateISO(now); const hasEntry = state.ressentis.some(r => r.date === todayStr); const inputCard = document.querySelector('.card.neon-border'); if (hasEntry) { inputCard.innerHTML = `

> LOG_ENTRY ${todayStr}

[ COMPLETED ]

Entry recorded for today.

`; } else if (!isLateEnough) { // Calculate time until 18:00 const target = new Date(); target.setHours(18, 0, 0, 0); const diff = Math.ceil((target - now) / (1000 * 60)); // minutes const hours = Math.floor(diff / 60); const mins = diff % 60; inputCard.innerHTML = `

> LOG_ENTRY ${todayStr}

[ LOCKED ]

> PROTOCOL: Inputs allowed after 18:00.

> T-MINUS: ${hours}h ${mins}m

`; } // Else: show default form (which is static in HTML, so we do nothing to overwrite it if it's there) } // Interactions window.setRating = function (val) { state.selectedRating = val; document.querySelectorAll('.btn-rating').forEach(b => b.classList.remove('selected')); document.querySelector(`.btn-rating[data-val="${val}"]`).classList.add('selected'); }; window.setEmoji = function (emoji) { state.selectedEmoji = emoji; document.getElementById('selected-emoji').textContent = emoji; document.querySelectorAll('.btn-emoji').forEach(b => b.classList.remove('selected')); }; window.submitEntry = async function () { if (!state.selectedRating) { setStatus("Error: Rating required."); return; } const todayStr = formatDateISO(new Date()); const comment = document.getElementById('comment-input').value; let existingIndex = state.ressentis.findIndex(r => r.date === todayStr); const entry = { date: todayStr, dose: state.todayDose, rating: state.selectedRating, emoji: state.selectedEmoji || '', commentaire: comment }; if (existingIndex >= 0) { state.ressentis[existingIndex] = entry; } else { state.ressentis.push(entry); } // Start Terminal Animation const inputCard = document.querySelector('.card.neon-border'); // We run animation and save in parallel, but ensuring min animation time const animationPromise = runTerminalSequence(inputCard); const savePromise = saveData(true); // pass true to skip internal reload await Promise.all([animationPromise, savePromise]); // After both done, reload full app loadData(); }; async function saveData(skipReload = false) { setStatus("Saving..."); try { await fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-auth-token': state.authToken }, body: JSON.stringify(state.ressentis) }); setStatus("Success: Saved to Server."); if (!skipReload) loadData(); } catch (e) { setStatus("Save Error: " + e.message); } } function runTerminalSequence(container) { return new Promise(resolve => { // 1. Create Overlay const overlay = document.createElement('div'); overlay.className = 'terminal-overlay'; overlay.innerHTML = `
INITIALIZING UPLINK...
[....................]
`; container.style.position = 'relative'; // Ensure absolute pos works container.appendChild(overlay); const statusEl = overlay.querySelector('.terminal-status'); const barEl = overlay.querySelector('.progress-bar-text'); const logsEl = overlay.querySelector('.terminal-logs'); const addLog = (msg) => { const div = document.createElement('div'); div.innerText = `> ${msg}`; logsEl.prepend(div); }; const steps = [ { t: 50, msg: "ENCRYPTING DATA PACKET...", progress: 2 }, { t: 200, msg: "ESTABLISHING SECURE TUNNEL...", progress: 5 }, { t: 400, msg: "HANDSHAKE_ACK_RECEIVED", progress: 8 }, { t: 550, msg: "UPLOADING TO MAINFRAME...", progress: 12 }, { t: 700, msg: "VERIFYING CHECKSUM...", progress: 16 }, { t: 800, msg: "SYNC COMPLETED.", progress: 20 } ]; let stepIndex = 0; function nextStep() { if (stepIndex >= steps.length) { // Finish statusEl.innerText = "ACCESS GRANTED"; statusEl.classList.add('flash-success'); setTimeout(() => { resolve(); }, 300); return; } const step = steps[stepIndex]; setTimeout(() => { statusEl.innerText = step.msg.split('...')[0]; addLog(step.msg); // Update bar const filled = "█".repeat(step.progress); const empty = ".".repeat(20 - step.progress); barEl.innerText = `[${filled}${empty}]`; stepIndex++; nextStep(); }, step.t - (stepIndex > 0 ? steps[stepIndex - 1].t : 0)); // Delta time } nextStep(); }); } window.refreshData = function () { loadData(); } window.downloadBackup = function () { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(state.ressentis, null, 2)); const downloadAnchorNode = document.createElement('a'); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", "ressentis_backup.json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); } window.toggleComment = function (commentId) { const commentEl = document.getElementById(commentId); const toggleEl = event.target; // The clicked "+/-" element if (commentEl.classList.contains('expanded')) { // Collapse commentEl.classList.remove('expanded'); toggleEl.textContent = '+'; } else { // Expand commentEl.classList.add('expanded'); toggleEl.textContent = '−'; } }