add supply

This commit is contained in:
2026-05-13 12:59:35 +05:30
parent fcf7654e78
commit 0bb486223e
6 changed files with 359 additions and 8 deletions
+4
View File
@@ -215,6 +215,8 @@ type Props = {
matchmakerSource: MatchmakerLogSource;
matchmakerContent: string;
matchmakerError: string | null;
/** Ledger: add-system-supply action failed (URL `supplyErr=1`). */
supplyError: boolean;
};
function StatCard({
@@ -264,6 +266,7 @@ export function AdminDashboard({
matchmakerSource,
matchmakerContent,
matchmakerError,
supplyError,
}: Props) {
const [hideNoWinner, setHideNoWinner] = useState(true);
const [lbSortKey, setLbSortKey] = useState<LeaderboardSortKey>("wins");
@@ -735,6 +738,7 @@ export function AdminDashboard({
participantRaw={participantRaw}
ledgerFrom={ledgerFrom}
ledgerTo={ledgerTo}
supplyError={supplyError}
/>
) : (
<section className="space-y-3">
+167 -8
View File
@@ -1,10 +1,13 @@
"use client";
import Link from "next/link";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { ClickableUserId } from "@/components/clickable-user-id";
import { buildDashboardHref } from "@/lib/dashboard-search-url";
import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc";
import {
formatRcBalanceWithCoins,
formatRcLabelFromCoinsBigInt,
} from "@/lib/coins-rc";
import { auditMatchEconomics } from "@/lib/ledger-match-audit";
import {
computeLedgerIntegrity,
@@ -21,6 +24,7 @@ import {
type LedgerSortOrder,
} from "@/lib/ledger-table-view";
import { buildLedgerScopedDailyCumulativeSeries } from "@/lib/ledger-scoped-daily-series";
import { addSystemSupply } from "@/app/actions/add-system-supply";
import { LedgerRangeCumulativeChart } from "@/components/ledger-range-cumulative-chart";
import type { DbTransaction, DbUser } from "@/types/database";
@@ -86,6 +90,7 @@ type Props = {
participantRaw: string | null;
ledgerFrom: string;
ledgerTo: string;
supplyError: boolean;
};
export function AdminLedger({
@@ -104,6 +109,7 @@ export function AdminLedger({
participantRaw,
ledgerFrom,
ledgerTo,
supplyError,
}: Props) {
const usernameById = useMemo(() => {
const m = new Map<number, string | null>();
@@ -113,6 +119,13 @@ export function AdminLedger({
return m;
}, [users]);
const systemAccount = useMemo(() => {
for (const u of users) {
if (Number(u.id) === 1) return u;
}
return null;
}, [users]);
const { integrityGlobal, userNets } = useMemo(() => {
if (ledgerGlobalSummary == null) {
return {
@@ -240,6 +253,25 @@ export function AdminLedger({
);
const pageNavItems = ledgerPaginationItems(ledgerPage, ledgerTotalPages);
const [supplyModalOpen, setSupplyModalOpen] = useState(false);
useEffect(() => {
if (supplyError) {
setSupplyModalOpen(true);
}
}, [supplyError]);
useEffect(() => {
if (!supplyModalOpen) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setSupplyModalOpen(false);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [supplyModalOpen]);
const navLinkClass =
"rounded px-2 py-1 font-medium text-sky-700 underline decoration-sky-400/50 underline-offset-2 hover:bg-sky-50 dark:text-sky-300 dark:hover:bg-sky-950/40";
const navMutedClass = "rounded px-2 py-1 text-zinc-400 dark:text-zinc-500";
@@ -275,12 +307,23 @@ export function AdminLedger({
: "border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/40",
].join(" ")}
>
<p className="text-xs font-medium uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
Σ user nets vs net issued
</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
All transactions
</p>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-xs font-medium uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
Σ user nets vs net issued
</p>
<p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
All transactions
</p>
</div>
<button
type="button"
onClick={() => setSupplyModalOpen(true)}
className="shrink-0 rounded-md border border-zinc-300 bg-white/90 px-2.5 py-1 text-xs font-medium text-zinc-800 shadow-sm hover:bg-white dark:border-zinc-600 dark:bg-zinc-900/90 dark:text-zinc-100 dark:hover:bg-zinc-900"
>
Add supply
</button>
</div>
<div className="mt-2 text-sm tabular-nums text-zinc-900 dark:text-zinc-100">
<p className="font-mono">
{formatBigAmount(integrityGlobal.sumUserNets)}
@@ -956,6 +999,122 @@ export function AdminLedger({
</div>
) : null}
{supplyModalOpen ? (
<div
className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/40 p-4"
role="presentation"
onClick={() => setSupplyModalOpen(false)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="supply-modal-title"
className="w-full max-w-md rounded-xl border border-zinc-200 bg-white p-6 shadow-lg dark:border-zinc-700 dark:bg-zinc-900"
onClick={(e) => e.stopPropagation()}
>
<h2
id="supply-modal-title"
className="text-lg font-semibold text-zinc-900 dark:text-zinc-50"
>
Add system supply
</h2>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Credits user id{" "}
<span className="font-mono">1</span>
{systemAccount?.username ? (
<span>
{" "}
(<span className="font-mono">{systemAccount.username}</span>)
</span>
) : null}
: mint row (<span className="font-mono">to = 1</span>,{" "}
<span className="font-mono">remarks = supply</span>), RC increases
by the same coin amount.
</p>
<p className="mt-2 text-xs text-zinc-500 dark:text-zinc-500">
Current RC:{" "}
<span className="font-mono text-zinc-800 dark:text-zinc-200">
{systemAccount
? formatRcBalanceWithCoins(systemAccount.rc)
: "— (user 1 not in loaded set)"}
</span>
</p>
{supplyError ? (
<p
className="mt-3 text-sm text-red-600 dark:text-red-400"
role="alert"
>
Could not add supply. Use a positive whole number of coin units
(no decimals), and ensure the database and service key are
available.
</p>
) : null}
<form
action={addSystemSupply}
className="mt-5 space-y-4"
>
<input type="hidden" name="tab" value="ledger" />
<input
type="hidden"
name="highlightId"
value={highlightId ?? ""}
/>
<input
type="hidden"
name="participantRaw"
value={participantRaw ?? ""}
/>
<input type="hidden" name="ledgerFrom" value={ledgerFrom} />
<input type="hidden" name="ledgerTo" value={ledgerTo} />
<input
type="hidden"
name="ledgerPage"
value={String(ledgerPage)}
/>
<input
type="hidden"
name="ledgerPageSize"
value={String(ledgerPageSize)}
/>
<input type="hidden" name="ledgerSort" value={ledgerSort} />
<input type="hidden" name="ledgerOrder" value={ledgerOrder} />
<div>
<label
htmlFor="supply-amount-modal"
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300"
>
Coin amount
</label>
<input
id="supply-amount-modal"
name="supplyAmount"
type="text"
inputMode="numeric"
autoComplete="off"
placeholder="e.g. 4000"
className="mt-1 w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm tabular-nums text-zinc-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-1 focus:ring-sky-500 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-100"
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button
type="button"
onClick={() => setSupplyModalOpen(false)}
className="rounded-md border border-zinc-300 bg-white px-4 py-2 text-sm font-medium text-zinc-800 shadow-sm hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-100 dark:hover:bg-zinc-800"
>
Cancel
</button>
<button
type="submit"
className="rounded-md border border-emerald-600 bg-emerald-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 dark:border-emerald-700 dark:bg-emerald-800 dark:hover:bg-emerald-700"
>
Submit
</button>
</div>
</form>
</div>
</div>
) : null}
</section>
);
}