@aglyn/plugins-workflows 1.0.0-beta.201 → 1.0.0-beta.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-workflows",
3
- "version": "1.0.0-beta.201",
3
+ "version": "1.0.0-beta.202",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,17 +25,17 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.201",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.201",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.201",
31
- "@aglyn/shared-ui-next": "1.0.0-beta.201",
32
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
33
- "@aglyn/shared-util-email": "1.0.0-beta.201",
34
- "@aglyn/shared-util-http": "1.0.0-beta.201",
35
- "@aglyn/shared-util-timestamp": "1.0.0-beta.201",
36
- "@aglyn/tenant-data-admin": "1.0.0-beta.201",
37
- "@aglyn/tenant-feature-instance": "1.0.0-beta.201",
38
- "@aglyn/tenant-runtime": "1.0.0-beta.201",
28
+ "@aglyn/aglyn": "1.0.0-beta.202",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.202",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.202",
31
+ "@aglyn/shared-ui-next": "1.0.0-beta.202",
32
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.202",
33
+ "@aglyn/shared-util-email": "1.0.0-beta.202",
34
+ "@aglyn/shared-util-http": "1.0.0-beta.202",
35
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.202",
36
+ "@aglyn/tenant-data-admin": "1.0.0-beta.202",
37
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.202",
38
+ "@aglyn/tenant-runtime": "1.0.0-beta.202",
39
39
  "@swc/helpers": "0.5.23"
40
40
  },
41
41
  "peerDependencies": {
@@ -23,6 +23,7 @@ import { collection, limit, orderBy, query, where } from "firebase/firestore";
23
23
  import { useParams } from "next/navigation";
24
24
  import { useEffect, useMemo, useState } from "react";
25
25
  import { ceilingedWindow, useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-instance";
26
+ import { useResolvedActivityActors } from "@aglyn/tenant-feature-instance/hooks/use-resolved-activity-actors";
26
27
  import { activityActorLabel, activityHref, activityPrimaryText } from "@aglyn/aglyn/app-utils/activity-presenter";
27
28
  import { pluginDocsHelp } from "@aglyn/aglyn";
28
29
  /**
@@ -162,11 +163,13 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
162
163
  targetId,
163
164
  hostId
164
165
  ]);
165
- const items = useMemo(()=>sorted.slice(page * pageSize, page * pageSize + pageSize), [
166
+ const pageItems = useMemo(()=>sorted.slice(page * pageSize, page * pageSize + pageSize), [
166
167
  sorted,
167
168
  page,
168
169
  pageSize
169
170
  ]);
171
+ // The page's rows only: a uid off the page is not asked about.
172
+ const items = useResolvedActivityActors(hostId, pageItems);
170
173
  // Three states, not two: a read that never happened must not be reported as
171
174
  // an empty history.
172
175
  const unreadable = status === 'error' || !hostId;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/host-activity-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink, CardDisplay, type HelpTipContent } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n Alert,\n Button,\n Link,\n List,\n ListItem,\n ListItemText,\n Stack,\n Typography,\n} from '@mui/material'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n ceilingedWindow,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n activityActorLabel,\n activityHref,\n activityPrimaryText,\n} from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface HostActivityCardProps {\n hostId: string\n /** Show only entries for this target id (e.g. a screen detail page). */\n targetId?: string\n header?: string\n /** When set, a \"View all activity\" link renders under the list (AGL-249). */\n viewAllHref?: string\n /** Overrides the default help affordance on the card header. */\n help?: HelpTipContent\n}\n\n/**\n * Rows fetched before the client-side sort and page slice.\n *\n * A CEILING, and the read asks for one document more than it so the card can\n * say when the window was not the whole story — `length >= WINDOW` is wrong at\n * exactly the collection size that equals the ceiling.\n */\nconst WINDOW = 200\n\n/**\n * Recent user activity (AGL-118): newest-first feed from\n * `hosts/{hostId}/activity`, optionally filtered to one target so detail\n * pages show just their own history.\n *\n * ## The \"Page Activity\" card reported nothing on a page that had changed\n *\n * This is AGL-2292 — fixed in the sibling `OrgActivityCard`, never applied\n * here. The query was `limit(200)` with NO `orderBy`. Firestore then returns\n * documents in DOCUMENT-ID order, `useHostActivityLogger` appends with\n * `addDoc()`, so the ids are effectively random: the window was a\n * pseudo-random SAMPLE of the collection rather than its newest page, and the\n * client sort below dutifully ordered that sample and sliced the top off it —\n * which made the result look right and be wrong.\n *\n * Measured on a production host: 996 activity documents, so a 200-row window\n * covers the lexicographically-lowest ids and ends at `ClSZ…`. A screen whose\n * five entries have ids beginning `Y`, `h`, `j`, `k`, `t` has every one of\n * them past that boundary — the card fetches 200 rows, matches 0, and says\n * \"No activity yet\" about a page edited that morning.\n *\n * ## Why the targeted query filters SERVER-side\n *\n * Adding `orderBy` alone is not the fix here, and the difference only shows up\n * on the per-target card. The window is host-wide, so 200 newest host-wide\n * entries still have to contain the target's own. On that same host it would\n * have surfaced 1 of the screen's 5 entries, and 40 of the 108 targets with\n * real activity would STILL have rendered \"No activity yet\" — a quieter\n * version of the same lie. A target that has not been touched recently is\n * exactly the one whose history you open the card to read.\n *\n * So `targetId` becomes a `where` and spends the window on the target instead\n * of on the host. That predicate needs no new index: an equality filter with\n * no `orderBy` is served by the automatic single-field index, which is\n * deliberate — a composite (`target.id` ASC, `createdAt` DESC) would be\n * FILE-ONLY until someone ran `firebase deploy --only firestore:indexes` by\n * hand, and every read in between would throw `failed-precondition`. The\n * ordering is done by the client sort below, which is safe at this size: the\n * busiest target in production holds 55 entries against a 200-row window.\n *\n * The un-targeted (dashboard) query has no such excuse and takes the AGL-2292\n * one-liner: `orderBy('createdAt', 'desc')`, single-field, no config change.\n *\n * ## \"Found nothing\" vs \"could not look\"\n *\n * Both used to render the empty state, and so did the first paint before any\n * snapshot arrived. A refused or failed listen now says so and offers a retry\n * (the `used-by-card` shape), because an audit feed that invents a confident\n * zero is worse than one that admits it is blind.\n *\n * ## Why the \"Show N more\" expander became the shared footer\n *\n * It was a FOURTH pagination grammar for the same act — beside the `DataGrid`\n * footer, the shared `ListPagination`, and the \"Load more\" the DAM grid keeps —\n * and it escaped the guard that exists to stop exactly that, on spelling: the\n * check looks for the literal `'Load more'` and this button said\n * `Show ${more} more`.\n *\n * On its own terms it was also the weakest of the four. It only ever grew, so\n * a reader who opened forty rows could not get back to ten without remounting\n * the card; it offered no size control, which made it one of the two grammars\n * that never let a reader choose; it could not state a total even though every\n * row was already in hand; and it said nothing when `WINDOW` bit, so a busy\n * target read as though two hundred entries were all there were.\n *\n * The footer answers all four, and the rows stay a client SLICE — see `sorted`,\n * where paging is free because the window is already fetched.\n */\nexport function HostActivityCard(props: HostActivityCardProps) {\n const {\n hostId,\n targetId,\n header = 'Recent Activity',\n viewAllHref,\n help = pluginDocsHelp('consoleTour', {\n anchor: '#a-sites-dashboard',\n excerpt:\n 'Changes made to this site from the console — publishes, media ' +\n 'saves, member changes — newest first.',\n }),\n } = props\n const { orgSlug, host } = useParams<{ orgSlug: string; host: string }>()\n const firestore = useFirestore()\n const [attempt, setAttempt] = useState(0)\n const { data: entries, status } = useFirestoreCollection<any>(\n () => {\n // `strictNullChecks` is off repo-wide, so an absent hostId would reach\n // `collection()` as a falsy path segment. Refuse to build the query\n // instead, and report it as unreadable below — never as \"no activity\".\n if (!hostId) return null\n const base = collection(firestore, 'hosts', hostId, 'activity')\n // `WINDOW + 1` on both: the extra document is the PROBE, and it is what\n // turns \"there may be more\" into a fact the card can state. It is never\n // rendered — `ceilingedWindow` drops it below.\n return targetId\n ? query(base, where('target.id', '==', targetId), limit(WINDOW + 1))\n : query(base, orderBy('createdAt', 'desc'), limit(WINDOW + 1))\n },\n [firestore, hostId, targetId, attempt],\n { idField: '$id' },\n )\n /*\n * The client sort survives the server-side ordering rather than being\n * replaced by it. It is what orders the TARGETED query (which cannot carry\n * an `orderBy` without a composite index — see above), and on the\n * un-targeted one it still catches a just-written entry whose timestamp the\n * local snapshot has not resolved. Sorting 200 items costs nothing.\n */\n /**\n * Paging is FREE here, which is why it is a slice and not a second query\n * (AGL-2486). The read above already fetches `WINDOW` rows and this then\n * threw all but a page of them away — 190 documents paid for and discarded\n * on every render of this card. Showing more of what is already in hand\n * costs nothing; only running past `WINDOW` would cost a read, and the\n * \"View all\" link exists for that.\n *\n * Sorting is safe here in a way it is not on a server-paged list: these rows\n * are the whole window, not a slice of one.\n */\n const { rows: windowRows, truncated } = ceilingedWindow<any>(entries, WINDOW)\n const sorted = useMemo(\n () =>\n [...windowRows].sort(\n (a, b) => (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n ),\n // `windowRows` is a fresh array each render; the snapshot behind it is not.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [entries],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // Back to the first page when the card is pointed at a different subject —\n // otherwise a previous screen's page three opens the next one three pages\n // in, over a history that may not have three.\n useEffect(() => setPage(0), [targetId, hostId])\n const items = useMemo(\n () => sorted.slice(page * pageSize, page * pageSize + pageSize),\n [sorted, page, pageSize],\n )\n // Three states, not two: a read that never happened must not be reported as\n // an empty history.\n const unreadable = status === 'error' || !hostId\n\n return (\n <CardDisplay\n header={header}\n help={help}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {unreadable ? (\n <Stack spacing={1.5} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"warning\" sx={{ width: '100%' }}>\n {'Could not read the activity log. This is NOT the same as ' +\n 'nothing having happened — do not treat this as a record ' +\n 'that the page is unchanged.'}\n </Alert>\n <Button size=\"small\" onClick={() => setAttempt((n) => n + 1)}>\n {'Try again'}\n </Button>\n </Stack>\n ) : status === 'loading' ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading activity…'}\n </Typography>\n ) : sorted.length === 0 ? (\n /*\n * The WINDOW's length, not the page's. A page past the end of a\n * history that shrank while it was open holds no rows and is not an\n * empty history, and this card must never confuse the two.\n */\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No activity yet — changes made in the console appear here.'}\n </Typography>\n ) : (\n <>\n <List dense disablePadding>\n {items.map((entry) => {\n const href = activityHref(entry, { orgSlug, host })\n const label = activityPrimaryText(entry)\n return (\n <ListItem key={entry.$id} disableGutters dense>\n <ListItemText\n primary={\n href ? (\n <AppLink href={href} color=\"inherit\" underline=\"hover\">\n {label}\n </AppLink>\n ) : (\n label\n )\n }\n secondary={\n `${activityActorLabel(entry)} · ${\n entry.createdAt?.toDate?.().toLocaleString() ?? ''\n }` +\n // Run-log entries carry duration (wave v6).\n (entry.durationMs != null\n ? ` · ${entry.durationMs}ms`\n : '')\n }\n slotProps={\n entry.status === 'error'\n ? { primary: { color: 'error' } }\n : undefined\n }\n />\n </ListItem>\n )\n })}\n </List>\n {/* Free paging: these rows are already fetched — see `sorted`. */}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={items.length}\n // The window the card HOLDS, which it knows exactly. `truncated`\n // below is what says the history is larger than that.\n count={sorted.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {/*\n * Two different sentences because the two queries answer\n * differently. The un-targeted one carries `orderBy('createdAt')`\n * and its window really is the newest entries; the targeted one\n * has no `orderBy` — deliberately, so it needs no composite\n * index — so its window is a document-id SAMPLE of the target's\n * history, and calling it \"most recent\" would be the same lie\n * AGL-2292 was.\n */}\n {targetId\n ? `Showing ${WINDOW} entries for this item. It has more — ` +\n 'these are not necessarily the newest; open the full ' +\n 'activity log for the ordered history.'\n : `Showing this site’s ${WINDOW} most recent entries. There ` +\n 'is more history than that — open the full activity log to ' +\n 'read further back.'}\n </Typography>\n ) : null}\n </>\n )}\n {viewAllHref ? (\n <Typography variant=\"body2\" sx={{ mt: 1 }}>\n <Link href={viewAllHref} color=\"primary\" underline=\"hover\">\n {'View all activity'}\n </Link>\n </Typography>\n ) : null}\n </CardDisplay>\n )\n}\nHostActivityCard.displayName = 'HostActivityCard'\n\nexport default HostActivityCard\n"],"names":["AppLink","CardDisplay","ListPagination","TABLE_PAGE_SIZE_DEFAULT","Alert","Button","Link","List","ListItem","ListItemText","Stack","Typography","collection","limit","orderBy","query","where","useParams","useEffect","useMemo","useState","ceilingedWindow","useFirestore","useFirestoreCollection","activityActorLabel","activityHref","activityPrimaryText","pluginDocsHelp","WINDOW","HostActivityCard","props","hostId","targetId","header","viewAllHref","help","anchor","excerpt","orgSlug","host","firestore","attempt","setAttempt","data","entries","status","base","idField","rows","windowRows","truncated","sorted","sort","a","b","createdAt","seconds","page","setPage","pageSize","setPageSize","items","slice","unreadable","contentGutterX","contentGutterY","contentBordered","spacing","sx","alignItems","severity","width","size","onClick","n","variant","color","length","dense","disablePadding","map","entry","href","label","disableGutters","primary","underline","secondary","toDate","toLocaleString","durationMs","slotProps","undefined","$id","rowCount","count","onPageChange","onPageSizeChange","mt","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/host-activity-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink, CardDisplay, type HelpTipContent } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n Alert,\n Button,\n Link,\n List,\n ListItem,\n ListItemText,\n Stack,\n Typography,\n} from '@mui/material'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useParams } from 'next/navigation'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n ceilingedWindow,\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport { useResolvedActivityActors } from '@aglyn/tenant-feature-instance/hooks/use-resolved-activity-actors'\nimport {\n activityActorLabel,\n activityHref,\n activityPrimaryText,\n} from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface HostActivityCardProps {\n hostId: string\n /** Show only entries for this target id (e.g. a screen detail page). */\n targetId?: string\n header?: string\n /** When set, a \"View all activity\" link renders under the list (AGL-249). */\n viewAllHref?: string\n /** Overrides the default help affordance on the card header. */\n help?: HelpTipContent\n}\n\n/**\n * Rows fetched before the client-side sort and page slice.\n *\n * A CEILING, and the read asks for one document more than it so the card can\n * say when the window was not the whole story — `length >= WINDOW` is wrong at\n * exactly the collection size that equals the ceiling.\n */\nconst WINDOW = 200\n\n/**\n * Recent user activity (AGL-118): newest-first feed from\n * `hosts/{hostId}/activity`, optionally filtered to one target so detail\n * pages show just their own history.\n *\n * ## The \"Page Activity\" card reported nothing on a page that had changed\n *\n * This is AGL-2292 — fixed in the sibling `OrgActivityCard`, never applied\n * here. The query was `limit(200)` with NO `orderBy`. Firestore then returns\n * documents in DOCUMENT-ID order, `useHostActivityLogger` appends with\n * `addDoc()`, so the ids are effectively random: the window was a\n * pseudo-random SAMPLE of the collection rather than its newest page, and the\n * client sort below dutifully ordered that sample and sliced the top off it —\n * which made the result look right and be wrong.\n *\n * Measured on a production host: 996 activity documents, so a 200-row window\n * covers the lexicographically-lowest ids and ends at `ClSZ…`. A screen whose\n * five entries have ids beginning `Y`, `h`, `j`, `k`, `t` has every one of\n * them past that boundary — the card fetches 200 rows, matches 0, and says\n * \"No activity yet\" about a page edited that morning.\n *\n * ## Why the targeted query filters SERVER-side\n *\n * Adding `orderBy` alone is not the fix here, and the difference only shows up\n * on the per-target card. The window is host-wide, so 200 newest host-wide\n * entries still have to contain the target's own. On that same host it would\n * have surfaced 1 of the screen's 5 entries, and 40 of the 108 targets with\n * real activity would STILL have rendered \"No activity yet\" — a quieter\n * version of the same lie. A target that has not been touched recently is\n * exactly the one whose history you open the card to read.\n *\n * So `targetId` becomes a `where` and spends the window on the target instead\n * of on the host. That predicate needs no new index: an equality filter with\n * no `orderBy` is served by the automatic single-field index, which is\n * deliberate — a composite (`target.id` ASC, `createdAt` DESC) would be\n * FILE-ONLY until someone ran `firebase deploy --only firestore:indexes` by\n * hand, and every read in between would throw `failed-precondition`. The\n * ordering is done by the client sort below, which is safe at this size: the\n * busiest target in production holds 55 entries against a 200-row window.\n *\n * The un-targeted (dashboard) query has no such excuse and takes the AGL-2292\n * one-liner: `orderBy('createdAt', 'desc')`, single-field, no config change.\n *\n * ## \"Found nothing\" vs \"could not look\"\n *\n * Both used to render the empty state, and so did the first paint before any\n * snapshot arrived. A refused or failed listen now says so and offers a retry\n * (the `used-by-card` shape), because an audit feed that invents a confident\n * zero is worse than one that admits it is blind.\n *\n * ## Why the \"Show N more\" expander became the shared footer\n *\n * It was a FOURTH pagination grammar for the same act — beside the `DataGrid`\n * footer, the shared `ListPagination`, and the \"Load more\" the DAM grid keeps —\n * and it escaped the guard that exists to stop exactly that, on spelling: the\n * check looks for the literal `'Load more'` and this button said\n * `Show ${more} more`.\n *\n * On its own terms it was also the weakest of the four. It only ever grew, so\n * a reader who opened forty rows could not get back to ten without remounting\n * the card; it offered no size control, which made it one of the two grammars\n * that never let a reader choose; it could not state a total even though every\n * row was already in hand; and it said nothing when `WINDOW` bit, so a busy\n * target read as though two hundred entries were all there were.\n *\n * The footer answers all four, and the rows stay a client SLICE — see `sorted`,\n * where paging is free because the window is already fetched.\n */\nexport function HostActivityCard(props: HostActivityCardProps) {\n const {\n hostId,\n targetId,\n header = 'Recent Activity',\n viewAllHref,\n help = pluginDocsHelp('consoleTour', {\n anchor: '#a-sites-dashboard',\n excerpt:\n 'Changes made to this site from the console — publishes, media ' +\n 'saves, member changes — newest first.',\n }),\n } = props\n const { orgSlug, host } = useParams<{ orgSlug: string; host: string }>()\n const firestore = useFirestore()\n const [attempt, setAttempt] = useState(0)\n const { data: entries, status } = useFirestoreCollection<any>(\n () => {\n // `strictNullChecks` is off repo-wide, so an absent hostId would reach\n // `collection()` as a falsy path segment. Refuse to build the query\n // instead, and report it as unreadable below — never as \"no activity\".\n if (!hostId) return null\n const base = collection(firestore, 'hosts', hostId, 'activity')\n // `WINDOW + 1` on both: the extra document is the PROBE, and it is what\n // turns \"there may be more\" into a fact the card can state. It is never\n // rendered — `ceilingedWindow` drops it below.\n return targetId\n ? query(base, where('target.id', '==', targetId), limit(WINDOW + 1))\n : query(base, orderBy('createdAt', 'desc'), limit(WINDOW + 1))\n },\n [firestore, hostId, targetId, attempt],\n { idField: '$id' },\n )\n /*\n * The client sort survives the server-side ordering rather than being\n * replaced by it. It is what orders the TARGETED query (which cannot carry\n * an `orderBy` without a composite index — see above), and on the\n * un-targeted one it still catches a just-written entry whose timestamp the\n * local snapshot has not resolved. Sorting 200 items costs nothing.\n */\n /**\n * Paging is FREE here, which is why it is a slice and not a second query\n * (AGL-2486). The read above already fetches `WINDOW` rows and this then\n * threw all but a page of them away — 190 documents paid for and discarded\n * on every render of this card. Showing more of what is already in hand\n * costs nothing; only running past `WINDOW` would cost a read, and the\n * \"View all\" link exists for that.\n *\n * Sorting is safe here in a way it is not on a server-paged list: these rows\n * are the whole window, not a slice of one.\n */\n const { rows: windowRows, truncated } = ceilingedWindow<any>(entries, WINDOW)\n const sorted = useMemo(\n () =>\n [...windowRows].sort(\n (a, b) => (b.createdAt?.seconds ?? 0) - (a.createdAt?.seconds ?? 0),\n ),\n // `windowRows` is a fresh array each render; the snapshot behind it is not.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [entries],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n // Back to the first page when the card is pointed at a different subject —\n // otherwise a previous screen's page three opens the next one three pages\n // in, over a history that may not have three.\n useEffect(() => setPage(0), [targetId, hostId])\n const pageItems = useMemo(\n () => sorted.slice(page * pageSize, page * pageSize + pageSize),\n [sorted, page, pageSize],\n )\n // The page's rows only: a uid off the page is not asked about.\n const items = useResolvedActivityActors(hostId, pageItems)\n // Three states, not two: a read that never happened must not be reported as\n // an empty history.\n const unreadable = status === 'error' || !hostId\n\n return (\n <CardDisplay\n header={header}\n help={help}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n >\n {unreadable ? (\n <Stack spacing={1.5} sx={{ alignItems: 'flex-start' }}>\n <Alert severity=\"warning\" sx={{ width: '100%' }}>\n {'Could not read the activity log. This is NOT the same as ' +\n 'nothing having happened — do not treat this as a record ' +\n 'that the page is unchanged.'}\n </Alert>\n <Button size=\"small\" onClick={() => setAttempt((n) => n + 1)}>\n {'Try again'}\n </Button>\n </Stack>\n ) : status === 'loading' ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading activity…'}\n </Typography>\n ) : sorted.length === 0 ? (\n /*\n * The WINDOW's length, not the page's. A page past the end of a\n * history that shrank while it was open holds no rows and is not an\n * empty history, and this card must never confuse the two.\n */\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No activity yet — changes made in the console appear here.'}\n </Typography>\n ) : (\n <>\n <List dense disablePadding>\n {items.map((entry) => {\n const href = activityHref(entry, { orgSlug, host })\n const label = activityPrimaryText(entry)\n return (\n <ListItem key={entry.$id} disableGutters dense>\n <ListItemText\n primary={\n href ? (\n <AppLink href={href} color=\"inherit\" underline=\"hover\">\n {label}\n </AppLink>\n ) : (\n label\n )\n }\n secondary={\n `${activityActorLabel(entry)} · ${\n entry.createdAt?.toDate?.().toLocaleString() ?? ''\n }` +\n // Run-log entries carry duration (wave v6).\n (entry.durationMs != null\n ? ` · ${entry.durationMs}ms`\n : '')\n }\n slotProps={\n entry.status === 'error'\n ? { primary: { color: 'error' } }\n : undefined\n }\n />\n </ListItem>\n )\n })}\n </List>\n {/* Free paging: these rows are already fetched — see `sorted`. */}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={items.length}\n // The window the card HOLDS, which it knows exactly. `truncated`\n // below is what says the history is larger than that.\n count={sorted.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n {truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {/*\n * Two different sentences because the two queries answer\n * differently. The un-targeted one carries `orderBy('createdAt')`\n * and its window really is the newest entries; the targeted one\n * has no `orderBy` — deliberately, so it needs no composite\n * index — so its window is a document-id SAMPLE of the target's\n * history, and calling it \"most recent\" would be the same lie\n * AGL-2292 was.\n */}\n {targetId\n ? `Showing ${WINDOW} entries for this item. It has more — ` +\n 'these are not necessarily the newest; open the full ' +\n 'activity log for the ordered history.'\n : `Showing this site’s ${WINDOW} most recent entries. There ` +\n 'is more history than that — open the full activity log to ' +\n 'read further back.'}\n </Typography>\n ) : null}\n </>\n )}\n {viewAllHref ? (\n <Typography variant=\"body2\" sx={{ mt: 1 }}>\n <Link href={viewAllHref} color=\"primary\" underline=\"hover\">\n {'View all activity'}\n </Link>\n </Typography>\n ) : null}\n </CardDisplay>\n )\n}\nHostActivityCard.displayName = 'HostActivityCard'\n\nexport default HostActivityCard\n"],"names":["AppLink","CardDisplay","ListPagination","TABLE_PAGE_SIZE_DEFAULT","Alert","Button","Link","List","ListItem","ListItemText","Stack","Typography","collection","limit","orderBy","query","where","useParams","useEffect","useMemo","useState","ceilingedWindow","useFirestore","useFirestoreCollection","useResolvedActivityActors","activityActorLabel","activityHref","activityPrimaryText","pluginDocsHelp","WINDOW","HostActivityCard","props","hostId","targetId","header","viewAllHref","help","anchor","excerpt","orgSlug","host","firestore","attempt","setAttempt","data","entries","status","base","idField","rows","windowRows","truncated","sorted","sort","a","b","createdAt","seconds","page","setPage","pageSize","setPageSize","pageItems","slice","items","unreadable","contentGutterX","contentGutterY","contentBordered","spacing","sx","alignItems","severity","width","size","onClick","n","variant","color","length","dense","disablePadding","map","entry","href","label","disableGutters","primary","underline","secondary","toDate","toLocaleString","durationMs","slotProps","undefined","$id","rowCount","count","onPageChange","onPageSizeChange","mt","displayName"],"mappings":"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"}