Files
takeyourmeds/public/app.js
T
Frank Schwenk 1cada42370 feat: replace Web Push with ntfy for medication reminders
Single notification path via ntfy HTTP publish for reliable Android delivery;
remove VAPID, push subscriptions, and SW push handlers. PWA settings show
topic subscribe link; humor texts and deep-link actions unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:42:58 +02:00

190 lines
5.8 KiB
JavaScript

import { api } from "./lib/api.js";
import { getToken, setToken, clearToken, isLoggedIn } from "./lib/auth.js";
import { setStreakBadge } from "./lib/badge.js";
import { enqueue, flushQueue } from "./lib/offlineQueue.js";
import { registerPwa, wirePwaInstall } from "./lib/pwa.js";
import { renderSlots, renderHeatmap, updateStats, showToast, showConfetti, showEasterEgg } from "./lib/ui.js";
const STATUS = { upcoming: "Noch nicht", pending: "Jetzt!", overdue: "Überfällig", snoozed: "Snoozed", taken: "Genommen", missed: "Verpasst" };
function showScreen(id) {
document.querySelectorAll(".screen").forEach((s) => { s.hidden = s.id !== id; });
}
function wirePinForm() {
const form = document.getElementById("pinForm");
const input = document.getElementById("pinInput");
const errEl = document.getElementById("loginError");
input.addEventListener("input", () => {
const digits = input.value.replace(/\D/g, "").slice(0, 6);
if (input.value !== digits) input.value = digits;
errEl.hidden = true;
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const pin = input.value.trim();
if (pin.length < 4) return;
errEl.hidden = true;
try {
const { token } = await api.login(pin);
setToken(token);
input.value = "";
await initApp();
} catch {
errEl.textContent = "Falscher PIN. Dein Gehirn auch.";
errEl.hidden = false;
input.value = "";
input.focus();
}
});
}
function wireTabs() {
document.querySelectorAll(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach((t) => t.classList.remove("active"));
document.querySelectorAll(".tabPanel").forEach((p) => p.hidden = true);
tab.classList.add("active");
const panel = document.getElementById(`tab-${tab.dataset.tab}`);
panel.hidden = false;
if (tab.dataset.tab === "history") loadHistory();
});
});
}
async function handleTake(slotId) {
const card = document.querySelector(`[data-slot="${slotId}"]`);
try {
if (!navigator.onLine) {
await enqueue({ slot_id: slotId, status: "taken" });
showToast("Offline gespeichert — wird synchronisiert.");
return;
}
const result = await api.log({ slot_id: slotId, status: "taken" });
card?.classList.add("dopamin");
showConfetti();
showToast(result.message);
if (result.new_milestones?.length) {
for (const m of result.new_milestones) {
showEasterEgg(m.title, "Achievement freigeschaltet!");
}
}
await refreshDashboard(result.stats);
} catch (e) {
await enqueue({ slot_id: slotId, status: "taken" });
showToast("Gespeichert — Sync folgt.");
}
}
async function handleSnooze(slotId, minutes) {
try {
const result = await api.snooze(slotId, minutes);
showToast(result.message || `Snooze +${minutes} Min`);
await refreshDashboard();
} catch (e) {
showToast("Snooze fehlgeschlagen.");
}
}
const MOTIVATION_CACHE_KEY = "medis-motivation-day";
function showMotivationFromCache(day) {
const el = document.getElementById("motivationText");
const cached = JSON.parse(sessionStorage.getItem(MOTIVATION_CACHE_KEY) || "null");
if (cached?.day === day && cached?.text) {
el.textContent = cached.text;
return true;
}
return false;
}
async function loadMotivation(day) {
try {
const motivation = await api.roast();
sessionStorage.setItem(MOTIVATION_CACHE_KEY, JSON.stringify({ day: motivation.day, text: motivation.text }));
document.getElementById("motivationText").textContent = motivation.text;
} catch {
document.getElementById("motivationText").textContent = "Dein Gehirn ist nicht kaputt. Es wartet nur auf den richtigen Treiber.";
}
}
async function refreshDashboard(stats) {
const today = await api.today();
renderSlots(today.slots, handleTake, handleSnooze);
if (!showMotivationFromCache(today.day)) {
document.getElementById("motivationText").textContent = "…";
loadMotivation(today.day);
}
const s = stats || await api.stats();
updateStats(s);
await setStreakBadge(s.streak);
}
async function loadHistory() {
const data = await api.history(90);
renderHeatmap(data.days);
updateStats(data.stats);
}
async function handleDeepLink() {
const params = new URLSearchParams(location.search);
const slot = params.get("slot");
if (params.get("action") === "take" && slot) {
await handleTake(slot);
history.replaceState({}, "", "/");
} else if (params.get("action") === "snooze" && slot) {
const minutes = parseInt(params.get("minutes") || "15", 10);
await handleSnooze(slot, minutes);
history.replaceState({}, "", "/");
}
}
async function initApp() {
showScreen("mainScreen");
wireTabs();
wirePwaInstall();
document.getElementById("logoutBtn").addEventListener("click", () => {
clearToken();
location.reload();
});
try {
const notify = await api.notifyConfig();
const topicEl = document.getElementById("notifyTopic");
const linkEl = document.getElementById("notifySubscribeLink");
if (notify.configured && notify.subscribe_url) {
topicEl.textContent = `Topic: ${notify.topic}`;
linkEl.href = notify.subscribe_url;
} else {
topicEl.textContent = "ntfy nicht konfiguriert (NTFY_URL + NTFY_TOPIC in .env).";
linkEl.hidden = true;
}
} catch {
document.getElementById("notifyTopic").textContent = "Notify-Config konnte nicht geladen werden.";
}
await flushQueue((data) => api.log(data));
await refreshDashboard();
await handleDeepLink();
}
navigator.serviceWorker?.addEventListener("message", (event) => {
if (event.data?.type === "SYNC_QUEUE") flushQueue((data) => api.log(data));
});
async function boot() {
registerPwa();
if (isLoggedIn()) {
try { await initApp(); return; } catch { clearToken(); }
}
showScreen("loginScreen");
wirePinForm();
}
boot();