FastAPI + SQLAlchemy/Alembic + MariaDB backend with Jinja2/htmx/Alpine server-rendered frontend. Multi-user via Google OAuth, daily habit tracking, monthly/weekly history views, Web Push reminders via APScheduler, and PWA support (manifest, service worker, offline caching). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
64 lines
2.1 KiB
JavaScript
64 lines
2.1 KiB
JavaScript
(function () {
|
|
function urlBase64ToUint8Array(base64String) {
|
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
const rawData = atob(base64);
|
|
const output = new Uint8Array(rawData.length);
|
|
for (let i = 0; i < rawData.length; i++) {
|
|
output[i] = rawData.charCodeAt(i);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function isPushSupported() {
|
|
return "serviceWorker" in navigator && "PushManager" in window;
|
|
}
|
|
|
|
async function getSubscriptionState() {
|
|
if (!isPushSupported()) return "unsupported";
|
|
const reg = await navigator.serviceWorker.ready;
|
|
const sub = await reg.pushManager.getSubscription();
|
|
return sub ? "subscribed" : "unsubscribed";
|
|
}
|
|
|
|
async function subscribePush() {
|
|
if (!isPushSupported()) {
|
|
alert("이 브라우저는 알림을 지원하지 않아요.");
|
|
return false;
|
|
}
|
|
const permission = await Notification.requestPermission();
|
|
if (permission !== "granted") {
|
|
alert("알림 권한이 허용되지 않았어요.");
|
|
return false;
|
|
}
|
|
|
|
const reg = await navigator.serviceWorker.ready;
|
|
const { publicKey } = await fetch("/api/push/vapid-public-key").then((r) => r.json());
|
|
const sub = await reg.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
});
|
|
await fetch("/api/push/subscribe", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(sub.toJSON()),
|
|
});
|
|
return true;
|
|
}
|
|
|
|
async function unsubscribePush() {
|
|
if (!isPushSupported()) return;
|
|
const reg = await navigator.serviceWorker.ready;
|
|
const sub = await reg.pushManager.getSubscription();
|
|
if (!sub) return;
|
|
await fetch("/api/push/unsubscribe", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ endpoint: sub.endpoint }),
|
|
});
|
|
await sub.unsubscribe();
|
|
}
|
|
|
|
window.habitPush = { getSubscriptionState, subscribePush, unsubscribePush };
|
|
})();
|