| | |
| | const DATE_DEBUT = new Date(2026, 0, 5); |
| |
|
| | 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 |
| | }; |
| |
|
| | |
| | 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}`; |
| | } |
| |
|
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | document.getElementById('current-date').textContent = new Date().toLocaleDateString(); |
| |
|
| | |
| | const savedToken = localStorage.getItem('parox_token'); |
| | if (savedToken) { |
| | |
| | attemptLogin(savedToken); |
| | } |
| |
|
| | |
| | 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); |
| | |
| | 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 => { |
| | |
| | html += `<div class="palier-block">`; |
| | html += `<div class="palier-header">=== ${palier.nom} ===</div>`; |
| | html += `<div class="palier-meta">Pattern : [${palier.pattern.join(', ')}] mg | Moyenne ≈ ${palier.moyenne} mg | Durée : ${palier.jours} jours</div>`; |
| |
|
| | 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); |
| |
|
| | |
| | if (runningDate <= today) { |
| | |
| | let entry = state.ressentis.find(r => r.date === isoDate); |
| | let lineClass = (runningDate.getTime() === today.getTime()) ? "today-line" : ""; |
| |
|
| | let uniqueId = `comment-${isoDate}`; |
| | daysHtml += `<div class="log-line ${lineClass}"> |
| | <span class="date">${dateStr}</span>: <span class="dose dose-${dose}">${dose} mg</span>`; |
| |
|
| | if (entry) { |
| | let stars = "★".repeat(entry.rating || 0); |
| | let comment = entry.commentaire || ''; |
| | let needsToggle = comment.length > 30; |
| |
|
| | daysHtml += ` <span class="stars">${stars}</span> <span class="emoji">${entry.emoji || ''}</span>`; |
| |
|
| | if (comment) { |
| | daysHtml += ` <span class="comment-wrapper">`; |
| | daysHtml += `<span class="comment" id="${uniqueId}">${comment}</span>`; |
| | if (needsToggle) { |
| | daysHtml += ` <span class="view-toggle" onclick="toggleComment('${uniqueId}')">+</span>`; |
| | } |
| | daysHtml += `</span>`; |
| | } |
| | } |
| | if (runningDate.getTime() === today.getTime()) { |
| | const timeStr = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); |
| | daysHtml += ` (aujourd'hui, ${timeStr})`; |
| | } |
| | daysHtml += `</div>`; |
| | } else { |
| | |
| | if (currentMonth !== runningDate.getMonth()) { |
| | if (futureBuffer.length > 0) { |
| | daysHtml += `<div class="future-line"><span class="dim">Jours à venir ${MOIS[currentMonth]} ${runningDate.getFullYear()} :</span> ${futureBuffer.join(', ')}</div>`; |
| | futureBuffer = []; |
| | } |
| | currentMonth = runningDate.getMonth(); |
| | } |
| | futureBuffer.push(`<span class="future-item">${runningDate.getDate()}(${dose}mg)</span>`); |
| | } |
| |
|
| | |
| | runningDate.setDate(runningDate.getDate() + 1); |
| | } |
| |
|
| | |
| | if (futureBuffer.length > 0) { |
| | |
| | let prev = new Date(runningDate); prev.setDate(prev.getDate() - 1); |
| | daysHtml += `<div class="future-line"><span class="dim">Jours à venir ${MOIS[prev.getMonth()]} ${prev.getFullYear()} :</span> ${futureBuffer.join(', ')}</div>`; |
| | } |
| |
|
| | html += daysHtml + `</div>`; |
| | }); |
| |
|
| | document.getElementById('schedule-output').innerHTML = html; |
| | } |
| |
|
| | function renderCalendar() { |
| | |
| | let html = `<div class="calendar-legend">Légende : <span class="dose-badge dose-20">█</span>=20mg <span class="dose-badge dose-10">▒</span>=10mg <span class="dose-badge dose-0">_</span>=0mg</div>`; |
| |
|
| | let currentDate = new Date(DATE_DEBUT); |
| | |
| | let totalDays = paliers.reduce((acc, p) => acc + p.jours, 0); |
| | let endDate = new Date(DATE_DEBUT); endDate.setDate(endDate.getDate() + totalDays); |
| |
|
| | |
| | |
| |
|
| | let monthGrid = []; |
| |
|
| | let runningDate = new Date(DATE_DEBUT); |
| | let palierIndex = 0; |
| | let dayInPalier = 0; |
| |
|
| | let currentBlock = { month: "", weeks: [] }; |
| | let currentWeek = Array(7).fill(null); |
| |
|
| | |
| | let absIndex = 0; |
| |
|
| | while (runningDate < endDate) { |
| | |
| | let curPalier = paliers[palierIndex]; |
| | let dose = curPalier.pattern[dayInPalier % curPalier.pattern.length]; |
| |
|
| | let jsMonth = runningDate.getMonth(); |
| | let monthName = MOIS[jsMonth]; |
| |
|
| | |
| | if (currentBlock.month !== monthName) { |
| | if (currentBlock.month !== "") { |
| | |
| | if (currentWeek.some(d => d !== null)) currentBlock.weeks.push(currentWeek); |
| | monthGrid.push(currentBlock); |
| | } |
| | currentBlock = { month: monthName, weeks: [] }; |
| | currentWeek = Array(7).fill(null); |
| | |
| | let dayOfWeek = (runningDate.getDay() + 6) % 7; |
| | |
| | |
| | |
| | } |
| |
|
| | let colIndex = (runningDate.getDay() + 6) % 7; |
| | if (colIndex === 0 && currentWeek.some(d => d !== null)) { |
| | |
| | currentBlock.weeks.push(currentWeek); |
| | currentWeek = Array(7).fill(null); |
| | } |
| |
|
| | |
| | currentWeek[colIndex] = { |
| | day: runningDate.getDate(), |
| | dose: dose, |
| | isToday: isSameDay(runningDate, new Date()) |
| | }; |
| |
|
| | |
| | runningDate.setDate(runningDate.getDate() + 1); |
| | dayInPalier++; |
| | if (dayInPalier >= curPalier.jours) { |
| | palierIndex++; |
| | dayInPalier = 0; |
| | if (palierIndex >= paliers.length) break; |
| | } |
| | } |
| | |
| | if (currentWeek.some(d => d !== null)) currentBlock.weeks.push(currentWeek); |
| | if (currentBlock.weeks.length > 0) monthGrid.push(currentBlock); |
| |
|
| | |
| | monthGrid.forEach(m => { |
| | html += `<div class="month-block">`; |
| | html += `<div class="month-title">--- ${m.month} ---</div>`; |
| | html += `<div class="week-row header-row"><span>L</span><span>M</span><span>M</span><span>J</span><span>V</span><span>S</span><span>D</span></div>`; |
| |
|
| | m.weeks.forEach(week => { |
| | html += `<div class="week-row">`; |
| | 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 += `<span class="${dayClass}"><span class="day-num">${cell.day.toString().padStart(2, '0')}</span><span class="${dClass}">${symbol}</span></span>`; |
| | } else { |
| | html += `<span class="day-cell empty"></span>`; |
| | } |
| | } |
| | html += `</div>`; |
| | }); |
| | html += `</div>`; |
| | }); |
| |
|
| | 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 = `<h3>> LOG_ENTRY ${todayStr}</h3><div class="large-text" style="color:var(--text-color)">[ COMPLETED ]</div><p>Entry recorded for today.</p>`; |
| | } else if (!isLateEnough) { |
| | |
| | const target = new Date(); target.setHours(18, 0, 0, 0); |
| | const diff = Math.ceil((target - now) / (1000 * 60)); |
| | const hours = Math.floor(diff / 60); |
| | const mins = diff % 60; |
| |
|
| | inputCard.innerHTML = `<h3>> LOG_ENTRY ${todayStr}</h3> |
| | <div class="large-text" style="color:var(--dim-color)">[ LOCKED ]</div> |
| | <p>> PROTOCOL: Inputs allowed after 18:00.</p> |
| | <p>> T-MINUS: ${hours}h ${mins}m</p>`; |
| | } |
| | |
| | } |
| |
|
| |
|
| |
|
| |
|
| | |
| |
|
| | 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); |
| | } |
| |
|
| | |
| | const inputCard = document.querySelector('.card.neon-border'); |
| |
|
| | |
| | const animationPromise = runTerminalSequence(inputCard); |
| | const savePromise = saveData(true); |
| |
|
| | await Promise.all([animationPromise, savePromise]); |
| |
|
| | |
| | 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 => { |
| | |
| | const overlay = document.createElement('div'); |
| | overlay.className = 'terminal-overlay'; |
| | overlay.innerHTML = ` |
| | <div class="terminal-status">INITIALIZING UPLINK...</div> |
| | <div class="progress-container"> |
| | <div class="progress-bar-text">[....................]</div> |
| | </div> |
| | <div class="terminal-logs"></div> |
| | `; |
| | container.style.position = 'relative'; |
| | 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) { |
| | |
| | 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); |
| |
|
| | |
| | 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)); |
| | } |
| |
|
| | 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; |
| |
|
| | if (commentEl.classList.contains('expanded')) { |
| | |
| | commentEl.classList.remove('expanded'); |
| | toggleEl.textContent = '+'; |
| | } else { |
| | |
| | commentEl.classList.add('expanded'); |
| | toggleEl.textContent = '−'; |
| | } |
| | } |
| |
|