BareGit
(() => {
    let etag = "";
    const path_match = window.location.pathname.match(
        /^\/g\/([0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/);
    const game_id = path_match ? path_match[1] : "";
    const state_url = game_id
        ? `/api/games/${game_id}/state` : "";
    const lifecycle = document.querySelector("#lifecycle");
    const map = document.querySelector("#map");
    const status = document.querySelector("#status");
    const pending = document.querySelector("#pending");
    const messages = document.querySelector("#messages");
    const STATUS_ORDER = [
        ["title", "Character"],
        ["dungeon_level", "Location"],
        ["hp", "HP", "max_hp"],
        ["power", "Power", "max_power"],
        ["armor_class", "Armor class"],
        ["experience_level", "Level"],
        ["experience", "Experience"],
        ["gold", "Gold"],
        ["time", "Turn"],
        ["alignment", "Alignment"],
        ["hunger", "Hunger"],
        ["carrying_capacity", "Encumbrance"],
        ["strength", "Strength"],
        ["dexterity", "Dexterity"],
        ["constitution", "Constitution"],
        ["intelligence", "Intelligence"],
        ["wisdom", "Wisdom"],
        ["charisma", "Charisma"],
        ["score", "Score"],
        ["weapon", "Weapon"],
        ["armor", "Armor"],
        ["terrain", "Terrain"],
        ["HD", "Hit dice"],
        ["version", "Version"],
    ];

    function displayText(field) {
        if(field && typeof field === "object")
        {
            return typeof field.text === "string" ? field.text : "";
        }
        return typeof field === "string" ? field : "";
    }

    function appendStatusField(label, value) {
        const item = document.createElement("div");
        item.className = "status-field";
        const name = document.createElement("dt");
        name.textContent = label;
        const text = document.createElement("dd");
        text.textContent = value;
        item.append(name, text);
        status.append(item);
    }

    function renderStatus(fields) {
        status.replaceChildren();
        const values = fields && typeof fields === "object" ? fields : {};
        const displayed = new Set();

        for(const [key, label, maximum] of STATUS_ORDER)
        {
            if(!Object.hasOwn(values, key)) continue;
            const current = displayText(values[key]);
            if(current === "") continue;
            let text = current;
            if(maximum && Object.hasOwn(values, maximum))
            {
                const maximum_text = displayText(values[maximum]);
                if(maximum_text !== "") text += ` / ${maximum_text}`;
                displayed.add(maximum);
            }
            appendStatusField(label, text);
            displayed.add(key);
        }

        const condition = values.condition;
        if(condition && Array.isArray(condition.active))
        {
            appendStatusField("Conditions",
                condition.active.length ? condition.active.join(", ") : "None");
            displayed.add("condition");
        }

        for(const [key, field] of Object.entries(values))
        {
            if(displayed.has(key)) continue;
            const text = displayText(field);
            if(text === "") continue;
            const label = key.replaceAll("_", " ");
            appendStatusField(label[0].toUpperCase() + label.slice(1), text);
        }

        if(!status.children.length)
        {
            const empty = document.createElement("p");
            empty.className = "status-empty";
            empty.textContent = "No status available yet.";
            status.append(empty);
        }
    }

    function show(value) {
        lifecycle.textContent = value.lifecycle || "unknown";
        map.textContent = (value.map && value.map.rows || []).join("\n");
        renderStatus(value.status);
        pending.textContent = value.pending
            ? JSON.stringify(value.pending, null, 2) : "None";
        messages.replaceChildren();
        for(const message of value.messages || [])
        {
            const item = document.createElement("li");
            item.textContent = message.text || "";
            messages.append(item);
        }
    }

    async function poll() {
        let retry_delay = 2000;
        if(!game_id)
        {
            lifecycle.textContent = "Invalid game URL";
            return;
        }
        try
        {
            const headers = etag ? {"If-None-Match": etag} : {};
            const response = await fetch(state_url, {
                headers, cache: "no-store",
            });
            if(response.status === 410)
            {
                window.location.replace("/");
                return;
            }
            if(response.status === 404)
            {
                lifecycle.textContent = "Game not found";
                return;
            }
            if(response.status !== 304)
            {
                if(!response.ok) throw new Error(`HTTP ${response.status}`);
                etag = response.headers.get("ETag") || "";
                show(await response.json());
            }
        }
        catch(error)
        {
            lifecycle.textContent = `Disconnected: ${error.message}`;
            etag = "";
            retry_delay = 5000;
        }
        setTimeout(poll, retry_delay);
    }

    poll();
})();