Revert the implicit-any typing of src/

Reverts 2428f44. The .d.ts files beside the JS modules and the
annotations threaded through src/ to satisfy noImplicitAny go;
tsconfig turns noImplicitAny off instead. The JS modules become
TypeScript in the next commit, so their types come from inference.

Kept from that commit, since they're runtime fixes rather than types:
- OrganizationDetail's website and email pills get an href and label
  (they arrive as bare strings, not link objects).
- The chapter map falls back to FALLBACK_COLOR for a region with no
  colour instead of painting its tiles black.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zaldimmar 2026-09-26 15:32:28 -05:00
parent 4618ad8e67
commit 5cedc68fd7
38 changed files with 340 additions and 1302 deletions

View file

@ -24,54 +24,23 @@
the reason shows up before the click rather than after it.
═══════════════════════════════════════════════════════════════ */
import { useCallback, useEffect, useState, type ReactNode } from "react";
import { useCallback, useEffect, useState } from "react";
import { del, get, patch } from "../../lib/api.js";
import { isUnauthorized, useAuth } from "../../lib/auth.tsx";
import { useNavigate } from "react-router-dom";
import { ROLES, ROLE_LABELS, ROLE_NOTES, type Role } from "../../lib/roles.ts";
/* An admin_users row as panel.js selects it, with its live session count. */
type PanelUser = {
id: number;
email: string;
name: string | null;
role: Role;
is_active: number;
created_at: string;
last_login_at: string | null;
sessions: number;
};
/* GET /api/admin/panel/overview. A count is null when its table
doesn't exist yet. */
type Overview = {
system: {
schemaVersion: number;
dbPath: string | null;
nodeVersion: string;
platform: string;
uptimeSeconds: number;
startedAt: string;
sessions: number | null;
roles: Role[];
};
content: Array<{ label: string; count: number | null }>;
users: PanelUser[];
};
type UserResponse = { user: PanelUser };
import { ROLES, ROLE_LABELS, ROLE_NOTES } from "../../lib/roles.ts";
/* SQLite hands back "2026-09-22 04:11:07" — UTC, but without the
marker that says so. Left alone, browsers read it as local time
and last-login drifts by the timezone offset. */
function when(value: string | null | undefined) {
function when(value) {
if (!value) return "—";
const iso = value.includes("T") ? value : `${value.replace(" ", "T")}Z`;
const date = new Date(iso);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
}
function uptime(seconds: number | null | undefined) {
function uptime(seconds) {
if (seconds == null) return "—";
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
@ -81,15 +50,7 @@ function uptime(seconds: number | null | undefined) {
return `${m}m`;
}
function Block({
title,
note,
children,
}: {
title: string;
note?: string;
children: ReactNode;
}) {
function Block({ title, note, children }) {
return (
<section className="mt-8 first:mt-0">
<div className="flex items-baseline gap-3">
@ -101,7 +62,7 @@ function Block({
);
}
function Stat({ label, value }: { label: string; value: ReactNode }) {
function Stat({ label, value }) {
return (
<div className="rounded-xl border border-[#138ba0]/20 bg-white px-4 py-3">
<div className="text-xs font-medium uppercase tracking-wider text-[#4a6b72]/70">
@ -116,23 +77,23 @@ export default function AdminPanel() {
const { user: me } = useAuth();
const navigate = useNavigate();
const [data, setData] = useState<Overview | null>(null);
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState(null);
// Which row is mid-request, and what went wrong on it. Scoped to
// the row so a failure on one account doesn't blank the table.
const [busyId, setBusyId] = useState<number | null>(null);
const [rowError, setRowError] = useState<{ id: number; message: string } | null>(null);
const [busyId, setBusyId] = useState(null);
const [rowError, setRowError] = useState(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
setData(await get<Overview>("/admin/panel/overview", { ttl: 0 }));
setData(await get("/admin/panel/overview", { ttl: 0 }));
} catch (err) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
setError((err instanceof Error && err.message) || "Couldn't load the panel.");
setError(err.message || "Couldn't load the panel.");
} finally {
setLoading(false);
}
@ -144,7 +105,7 @@ export default function AdminPanel() {
/* Replace the one row the server returns rather than refetching
the whole overview — the counts didn't change. */
function mergeUser(updated: PanelUser) {
function mergeUser(updated) {
setData((current) =>
current
? {
@ -155,37 +116,30 @@ export default function AdminPanel() {
);
}
async function run(id: number, work: () => Promise<PanelUser>) {
async function run(id, work) {
setBusyId(id);
setRowError(null);
try {
mergeUser(await work());
} catch (err) {
if (isUnauthorized(err)) return navigate("/admin/login", { replace: true });
setRowError({ id, message: (err instanceof Error && err.message) || "That didn't work." });
setRowError({ id, message: err.message || "That didn't work." });
} finally {
setBusyId(null);
}
}
const changeRole = (row: PanelUser, role: Role) =>
const changeRole = (row, role) =>
run(row.id, async () => (await patch(`/admin/panel/users/${row.id}`, { role })).user);
const setActive = (row, is_active) =>
run(
row.id,
async () => (await patch<UserResponse>(`/admin/panel/users/${row.id}`, { role })).user,
async () => (await patch(`/admin/panel/users/${row.id}`, { is_active })).user,
);
const setActive = (row: PanelUser, is_active: number) =>
run(
row.id,
async () =>
(await patch<UserResponse>(`/admin/panel/users/${row.id}`, { is_active })).user,
);
const revoke = (row: PanelUser) =>
run(
row.id,
async () => (await del<UserResponse>(`/admin/panel/users/${row.id}/sessions`)).user,
);
const revoke = (row) =>
run(row.id, async () => (await del(`/admin/panel/users/${row.id}/sessions`)).user);
if (loading) {
return (
@ -210,9 +164,6 @@ export default function AdminPanel() {
);
}
// Not loading and no error means the overview arrived.
if (!data) return null;
const { system, content, users } = data;
const activeSupers = users.filter(
(u) => u.role === "superadmin" && u.is_active === 1,
@ -303,7 +254,7 @@ export default function AdminPanel() {
<select
value={row.role}
disabled={locked || busy}
onChange={(e) => changeRole(row, e.target.value as Role)}
onChange={(e) => changeRole(row, e.target.value)}
className="rounded-lg border border-[#138ba0]/30 bg-white px-2 py-1 text-sm text-[#0f2f36] disabled:cursor-not-allowed disabled:bg-[#f6fbfc] disabled:text-[#4a6b72]/60"
title={
isMe