Add Web Push, guided start screen, SVG icons, chunked uploads, rename

- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished
  notifications while no app window is visible; settings with test button
- Guided new-session flow: task, craft target (website/app/game/old
  software/fresh), clean room or deep, description, model, folder
  (new, existing or cloned git repository); originals upload to original/
- Craft phase bar (craft_phase tool) for study/specify/build/check/ready
- Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB)
- All UI glyphs and emojis replaced by themeable SVG icons
- Rename dialog with manual name or model-generated suggestion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 21:03:26 +01:00
1 parent 013695722a
commit a0d0e707f2
48 files changed
+2724 -228

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
import { get, post } from "./api";
function b64ToBytes(b64: string): Uint8Array<ArrayBuffer> {
const s = atob(b64.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((b64.length + 3) % 4));
const out = new Uint8Array(new ArrayBuffer(s.length));
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
return out;
}
export const pushSupported = () => "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
/** Current push subscription of this device (null if none). */
export async function currentSubscription(): Promise<PushSubscription | null> {
if (!pushSupported()) return null;
const reg = await navigator.serviceWorker.ready;
return reg.pushManager.getSubscription();
}
/** Ask for permission, subscribe this device and register it with the server. */
export async function enablePush(): Promise<void> {
if (!pushSupported()) throw new Error("Push is not supported by this browser (on iOS: install the app to the home screen first).");
const perm = await Notification.requestPermission();
if (perm !== "granted") throw new Error("Notifications were not allowed.");
const reg = await navigator.serviceWorker.ready;
const { key } = await get<{ key: string }>("/push/key");
let sub = await reg.pushManager.getSubscription();
// A subscription for another server key must be replaced.
if (sub && sub.options.applicationServerKey) {
const cur = new Uint8Array(sub.options.applicationServerKey);
const want = b64ToBytes(key);
if (cur.length !== want.length || cur.some((b, i) => b !== want[i])) {
await sub.unsubscribe();
sub = null;
}
}
if (!sub) sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToBytes(key) });
await post("/push/subscribe", sub.toJSON());
localStorage.setItem("codecraft.push", "1");
}
export async function disablePush(): Promise<void> {
const sub = await currentSubscription();
if (sub) {
await fetch("/api/push/subscribe", { method: "DELETE", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: sub.endpoint }) });
await sub.unsubscribe();
}
localStorage.removeItem("codecraft.push");
}
export async function testPush(): Promise<number> {
const r = await post<{ sent: number }>("/push/test", {});
return r.sent;
}