diff --git a/app/cm_api.py b/app/cm_api.py index bea4d1b..05c8dc4 100644 --- a/app/cm_api.py +++ b/app/cm_api.py @@ -122,7 +122,10 @@ class CM_API: ) params = [limit, offset] - return jsonify(db.query(query, params)) + rows = db.query(query, params) + count_rows = db.query("SELECT COUNT(*) AS c FROM acc", []) + total = int(count_rows[0]["c"]) if count_rows else 0 + return jsonify({"rows": rows, "total": total}) except Exception as error: return self._handle_error(error, "Not Found"), 404 @@ -166,7 +169,10 @@ class CM_API: ) params = [limit, offset] - return jsonify(db.query(query, params)) + rows = db.query(query, params) + count_rows = db.query("SELECT COUNT(*) AS c FROM user", []) + total = int(count_rows[0]["c"]) if count_rows else 0 + return jsonify({"rows": rows, "total": total}) except Exception as error: return self._handle_error(error, "Not Found"), 404 diff --git a/web/app/page.tsx b/web/app/page.tsx index dc7a7fd..7a11641 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -9,6 +9,7 @@ export default async function AccountsPage() { ); diff --git a/web/app/users/page.tsx b/web/app/users/page.tsx index d5c9a3d..c085485 100644 --- a/web/app/users/page.tsx +++ b/web/app/users/page.tsx @@ -13,6 +13,7 @@ export default async function UsersPage() { ); diff --git a/web/components/accounts-table.tsx b/web/components/accounts-table.tsx index 66f8025..f4d89ee 100644 --- a/web/components/accounts-table.tsx +++ b/web/components/accounts-table.tsx @@ -16,6 +16,7 @@ import Toast, { type ToastMessage } from "./toast"; type Props = { initial: Acc[]; initialHasMore: boolean; + initialTotal: number; prefixPattern: string; }; type SortDir = "asc" | "desc"; @@ -61,6 +62,7 @@ function DeleteButton({ export default function AccountsTable({ initial, initialHasMore, + initialTotal, prefixPattern, }: Props) { const [sortDir, setSortDir] = useState("desc"); @@ -77,6 +79,11 @@ export default function AccountsTable({ // Accumulated rows from initial server-side fetch + every loadMore. const [rows, setRows] = useState(initial); const [hasMore, setHasMore] = useState(initialHasMore); + // Total row count in the DB; updated on every page fetch so it stays + // fresh as external writes (the cm99 monitor) add rows. Mutations + // adjust it locally so the header doesn't flash a stale count between + // the optimistic update and the next page-fresh count. + const [total, setTotal] = useState(initialTotal); const sentinelRef = useRef(null); const [optimistic, applyOptimistic] = useOptimistic( @@ -111,6 +118,7 @@ export default function AccountsTable({ const page = await refreshAccounts({ prefix: prefixPattern, dir: sortDir }); setRows(page.rows); setHasMore(page.hasMore); + setTotal(page.total); } finally { setRefreshing(false); } @@ -128,6 +136,7 @@ export default function AccountsTable({ }); setRows(page.rows); setHasMore(page.hasMore); + setTotal(page.total); } finally { setLoadingMore(false); } @@ -153,6 +162,7 @@ export default function AccountsTable({ .then((page) => { setRows((prev) => [...prev, ...page.rows]); setHasMore(page.hasMore); + setTotal(page.total); }) .catch((err) => console.error("loadMoreAccounts failed:", err)) .finally(() => setLoadingMore(false)); @@ -172,6 +182,7 @@ export default function AccountsTable({ if (result.ok) { const deleted = deleteTarget; setRows((prev) => prev.filter((r) => r.username !== deleted)); + setTotal((t) => Math.max(0, t - 1)); setDeleteTarget(null); setToast({ type: "success", message: `Account ${deleted} deleted` }); } else { @@ -183,9 +194,8 @@ export default function AccountsTable({ return (
setCreateOpen(true)} @@ -213,9 +223,8 @@ export default function AccountsTable({ return (
setCreateOpen(true)} @@ -435,23 +444,22 @@ function CardRow({ label, children }: { label: string; children: React.ReactNode } function PageHead({ - count, + total, loaded, - hasMore, onRefresh, refreshing, onAdd, }: { - count: number; + total: number; loaded: number; - hasMore: boolean; onRefresh: () => void; refreshing: boolean; onAdd: () => void; }) { - // count == loaded for now; kept separate so a future "showing X of Y" - // header (when we surface a server-side total) drops in cleanly. - const showHasMore = hasMore && loaded > 0; + // total = COUNT(*) from the API; loaded = how many rows the user has + // scrolled in so far. Show the partial count only while it differs + // from the total, so a fully-scrolled list reads cleanly. + const showLoaded = loaded > 0 && loaded < total; return (
@@ -461,10 +469,14 @@ function PageHead({

Accounts - {count} - {showHasMore && +} + {total}

+ {showLoaded && ( +

+ Showing {loaded} of {total} — keep scrolling to load more +

+ )}