|
|
|
|
|
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 = '−'; |
|
|
} |
|
|
} |
|
|
|