parox / src /public /script.js
glutamatt's picture
glutamatt HF Staff
fix
1d33c9a verified
// 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 += `<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);
// 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 += `<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; // Threshold for showing view more
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 {
// It is future, group by month like python script
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>`);
}
// 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 += `<div class="future-line"><span class="dim">Jours à venir ${MOIS[prev.getMonth()]} ${prev.getFullYear()} :</span> ${futureBuffer.join(', ')}</div>`;
}
html += daysHtml + `</div>`; // Close palier-block
});
document.getElementById('schedule-output').innerHTML = html;
}
function renderCalendar() {
// Generate multi-month grid
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);
// 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 += `<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) {
// 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 = `<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>`;
}
// 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 = `
<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'; // 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 = '−';
}
}