Skip to content

Admin: city comms, report chat composer, verification flow, jurisdiction and queue cleanups (shared 0.53.0) - #31

Merged
theobong merged 17 commits into
mainfrom
feat/admin-city-comms
Sep 22, 2026
Merged

theobong merged 17 commits into
mainfrom
feat/admin-city-comms

Conversation

@theobong

Copy link
Copy Markdown
Member

What changed

Operators can now talk to a report's neighbours from the admin dashboard: the report's "Chat" panel loads for real and has a box to post into it. The verification buttons on a report tell the truth about what they do and an "Approve" button is back for reports that cannot be sent to a city. The Jurisdictions detail's "Save & route" now keeps the operator note and the "@handle" it used to throw away, the Reports / Moderation / Users queue chips now count and filter on the server, and the dashboard's "Moderation" and "Mail" tiles lead with real totals. Admin only — nothing changes on web or mobile.

Before you start

  • Where: staging (admin.civfix.dev)
  • Sign in as: operator
  • Data: at least one report with photos and neighbour chat messages; one report whose city has no routing contact; one gov access request waiting in "Gov claims"
  • Size: 1903 counted lines — single PR at the author's request

Verify

[Admin]

  1. Open "Reports" — Expect: the first chip reads "Needs verification", is already selected, and carries a number; the list under "Reports" shows that many reports.
  2. Click a report with neighbour messages and scroll the left column to the "Chat" panel — Expect: the neighbours' messages are listed oldest first with their names, times and any photos, and the counter beside "Chat" matches how many rows you see. No "Could not load this" panel.
  3. Type "Checking with the city today." into the box at the bottom of "Chat" that reads "Message the neighbors in this report's chat…", then click "Post" — Expect: the box clears, a toast says "Message posted to the report chat", and your message appears as the newest row in the thread.
  4. Type another line and press Cmd+Enter (Ctrl+Enter on Windows) instead of clicking — Expect: it posts the same way.
  5. Look just under the box on a report that has a city contact on file — Expect: a small grey line reading "Tagging the city's @handle forwards this message to" followed by that report's department name, "by email." On a report with no contact on file that line is absent.
  6. On one of the neighbour messages, click "Remove", then click "Remove" in the "Remove message" box — Expect: a toast says "Message removed" and that row turns into a dashed "Message removed" placeholder.
  7. Open a report that has a city contact and has not been sent yet, look at the right column's "Send to jurisdiction" panel and click "Verify and send to city" — Expect: a box titled "Verify and send to the city?" whose text says it approves this report's verification, that it emails the report and its attached photos to the address shown, and that once this reporter has two approved reports their account is marked report-verified.
  8. Click "Verify and send" — Expect: a toast naming the address it went to, the pill top-right of the panel flips to "Approved", and the "Outreach" line in "Routed to" stops saying "Not sent".
  9. Open a report whose city has no routing contact — Expect: the panel shows the grey line "This jurisdiction has no routing contact on file. Reports are only forwarded to a contact on file.", there is no send button, and a green "Approve" button now sits to the left of "Reject".
  10. Click that "Approve", read the "Approve report" box, then click "Approve" — Expect: the box says it approves this report's verification, that it is not sent to the city, and that two approved reports mark the reporter report-verified; afterwards a toast ends in "· approved", the pill reads "Approved", and the "Approve" button disappears.
  11. Open a report whose panel button reads "Already sent" and is greyed out — Expect: "Approve" is offered beside "Reject" when the pill still reads "Not yet reviewed", and its box does not claim the report is unsent.
  12. Open a report whose panel button reads "Send again to jurisdiction", click it and confirm — Expect: the box is titled "Send to the city?" and only mentions the email; after it finishes the verification pill is unchanged (a resend never approves).
  13. Click "Reject" on a report that has already gone to the city — Expect: the "Reject report" box says the report was already emailed to the city and that rejecting does not recall that email.
  14. Open "Moderation", stay on the "Queue" tab and click the "User reports" chip — Expect: only citizen-reported items are listed, each titled "Reported report", "Reported comment", "Reported message" and so on, and the count beside "Queue" matches the rows.
  15. Switch to the "Gov claims" tab, pick a waiting claim, and click "Mark verified" on a check that already has an evidence line under it — Expect: the evidence box opens with that text already filled in, and after confirming the check reads "Verified" with the same evidence and note still shown.
  16. Click "Mark pending" on that check — Expect: it reads "Pending" again and its evidence and note lines are still there.
  17. Click "Approve" on that claim and read the "Approve …?" box — Expect: it says access is granted to the account for the contact address, that a placeholder account is created if that address has none, that the approved claim is what links the account to the jurisdiction (only when a jurisdiction is shown in "Applicant"), that existing sessions are signed out only if the role actually changes, how many of three checks are verified, and that it is written to the audit log.
  18. Find a claim whose contact address is blank — Expect: "Approve" is greyed out, hovering it says "This claim has no contact email, so there is no account to grant government access to.", and the same sentence appears in a note under the buttons.
  19. Open "Users" — Expect: the chip row reads "All", "Active", "Suspended", "Flagged", "Banned", "Deleted", each with a number; clicking "Banned" lists only banned accounts and clicking "Deleted" lists only deleted ones.
  20. Open "Jurisdictions", pick a place needing a contact, type a routing email, type "Called the clerk, they confirmed this inbox." into "Add a note for the next operator…", type a handle into the "@" box, then click "Save & route" — Expect: a toast reading "Contacts saved for" that place "· discovery task closed", and after the list refreshes reopening the same place shows the handle you typed still in the "@" box.
  21. Read the grey note under the buttons on that panel — Expect: it says "Save & route" saves the contacts, the note and the @handle, closes the discovery task, and queues an outreach digest when outreach digests are enabled, and that it does not email the reports already waiting.
  22. Look at each routing-contact card in "Routing contacts" — Expect: the small grey line under "Trash" reads "Illegal dumping" once (never "Dump, Illegal dumping"), and the cards for "Graffiti", "Encampment" and "Other" have no grey line at all.
  23. Open the dashboard (the civfix logo, top left) — Expect: the "Moderation" tile's headline is a number followed by "queued — user reports, held media, clusters and appeals", and the "Mail" tile's footer shows "Needs action" and "Inbox unread" with numbers.
  24. On the dashboard's "Live map", click a pin for a report that has not been verified yet — Expect: the card that slides in names the place and reads "Needs verification"; click an event pin and it reads "Cleanup event"; the "need attention" counter in the map header matches the red pins.

Regression

Working the reports queue — [Admin]

  1. Open "Reports" and click through the "In progress", "Completed", "Flagged" and "All" chips.
  2. Type a place name into "Search title, place, reporter…".
  3. Click "Load more" at the bottom of the list if it appears.
  4. Click a row — Expect: every chip's number matches its list, the search narrows the rows, and the detail opens with its status pill, photos, map, "Activity" timeline and "Reporter" card unchanged.

One report end to end — [Admin]

  1. On a report detail, use a "Quick status" button, then "Flag", then open "Message the city" and send a short line.
  2. Click "View conversation →" under "Routed to".
  3. Go back and click "Remove report", then confirm — Expect: the status pill and toasts behave as before, the follow-up lands on the city conversation in "Mail", and the removed report leaves the list.

Jurisdictions detail — [Admin]

  1. Open "Jurisdictions", click the "Needs mapping", "Routed" and "All" chips, change "Type" and "Sort", and search a GEOID.
  2. Open a place and click "Save draft" with nothing changed, then "Flag for review", then "Edit template" and save a template.
  3. Click the "Unmapped" row — Expect: the chips and sorting behave as before, "Save draft" with nothing changed says "Nothing to save yet", the flag and template toasts appear, and the unmapped triage panel still lists its waiting types.

Moderation queue and gov claims — [Admin]

  1. On the "Queue" tab click "All", "Image", "Pattern", "Appeal", "GPS", "Duplicate" and "High", pick an item and use "Keep" or "Approve", "Hold" and "Remove".
  2. On an appeal item use "Overturn" and "Uphold".
  3. On the "Gov claims" tab click "Pending", "Approved", "Rejected" and "All", then "Reject" a pending claim with a reason — Expect: every chip lists its own kind, each decision shows its toast and drops the item from the queue, and an already-decided claim shows the greyed buttons plus the "already approved"/"already rejected" note.

Mail — [Admin]

  1. Open "Mail", switch between the outreach and inbox views, open a thread, reply with Cmd+Enter, and use "Mark replied".
  2. Open "Edit template" and save — Expect: unchanged: the reply sends, the thread shows its delivery state, and the counts in the header match.

Users — [Admin]

  1. Open "Users", search a name, open an account, then use the status and role controls and open its "Reports", "Events" and "Messages" tabs.
  2. Click "Load more" — Expect: the account detail, its activity tabs and paging behave as before.

Dashboard — [Admin]

  1. Open the dashboard and click "Open" on each tile, plus a preview row inside "Jurisdictions", "Reports", "Events", "Users", "Moderation" and "Mail".
  2. Click a "Live map" pin and then "Open report →" — Expect: every tile and row lands on the right page with the right item selected, and the "Analytics" tile still shows its sparkline and numbers.

Not covered

  • Posting into a report's chat, removing a chat message, the "Needs verification" chip, the "User reports" chip and the "Banned" / "Deleted" chips all need the matching civfix-backend city-comms change merged and deployed to staging first. That pull request is not open yet, so until it ships those chips return an error on staging and "Post" fails — retest steps 1-6, 14 and 19 after it deploys.
  • The dashboard's "Moderation" headline number and the "Mail" tile's "Inbox unread" figure also come from that same backend change; until it deploys the "Moderation" tile keeps its plain label and "Inbox unread" is simply absent, which is the intended fallback.
  • Whether a message tagged with a city's "@handle" really arrives in the city's inbox — that is a live email round trip and cannot be confirmed from the dashboard.
  • Whether approving a gov claim really signs that account out, and the refusal messages for an unverified contact address or an operator address: all three need a real account in that state, which staging does not have seeded.
  • The "two approved reports mark the reporter report-verified" behaviour is the backend's; the plan only checks that the confirmation box says so.
  • The greyed-out "Already sent" and "Send again to jurisdiction" states depend on a real send or bounce having happened on staging first.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

Not safe to merge: a routing outage can persist report approval without sending the report, and explicit repository requirements remain unmet.

Fix All in Claude CodeFindings

  1. P1 Keep approval and routing together ▶
  2. P2 Load older chat messages ▶
  3. P2 Clarify loaded mail count ▶
  4. P2 Use the changeset workflow ▶
  5. P2 Remove new explanatory comments ▶
  6. P2 Use category color tokens ▶
Fix with agent prompt
### Issue 1
apps/admin/src/features/reports/reports-page.tsx:609-615
“Verify and send” persists approval before it starts the separate routing request. If routing fails, the report stays approved even though no jurisdiction received it, which can advance the reporter’s verification progress and remove the report from the state requiring routing. Make this one atomic server operation, or compensate by reversing approval when routing cannot complete. This must be addressed before merging.

### Issue 2
apps/admin/src/features/reports/use-reports.ts:123-126
This request loads only the newest 50 messages and does not use the returned cursor. The discussion view offers no way to load older messages and presents the loaded-window length as the chat count, so operators cannot inspect or moderate earlier discussion on active reports. This is non-blocking, but it makes moderation incomplete and the displayed count misleading.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 3
apps/admin/src/features/mail/mail-page.tsx:708
The header displays the number of loaded mail rows as a bare total while “Load more” remains available. Operators can read that partial page length as the full queue size. This is non-blocking, but it gives an inaccurate view of outstanding mail; retain a “+” or “loaded so far” treatment, or show a server-provided total.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 4
apps/admin/package.json:18
This directly changes the `@civfix/shared` version range and refreshes the lockfile without adding a changeset. The repository directive requires dependency version changes to use the changeset workflow. This repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 5
apps/admin/src/features/home/home-preview-presentation.ts:8-12
This change adds a new explanatory block comment. The repository directive says new code must not add comments; the same new-comment pattern also appears in the government-claim presentation helpers and on `SectionSummary.lead`. Keep the implementation self-explanatory and remove the new comments. This repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 6
apps/admin/src/styles/colors-and-type.css:93-100
This change replaces the cleanup category token reference with a raw hex value while retaining a literal category palette for changed presentation. The frontend styling directive requires design tokens and prohibits new raw hex colors. Source the category colors from the shared category-color mechanism or existing semantic variables. This repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1) · Last reviewed commit: "Keep the local Claude launch config out ..."

Comment on lines +609 to +615
verdict.mutate(
{ id: report.id, verdict: "approved" },
{
onSuccess: () => {
setApprovedLocally(true)
routeToJurisdiction(note)
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Keep approval and routing together

“Verify and send” persists approval before it starts the separate routing request. If routing fails, the report stays approved even though no jurisdiction received it, which can advance the reporter’s verification progress and remove the report from the state requiring routing. Make this one atomic server operation, or compensate by reversing approval when routing cannot complete. This must be addressed before merging.

Artifacts

Approval and routing failure harness

  • The authored harness reproduces the page’s approval-then-route request sequence against the shared API client, showing the two operations are independent.

Successful approval and routing output

  • The successful baseline records verdict then route and ends with the report approved and routed.

Routing failure after approval output

  • The forced route failure records HTTP 503 while the report remains approved and unrouted, with no compensating request.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/reports/reports-page.tsx
Line: 609-615

Comment:
**Keep approval and routing together**

“Verify and send” persists approval before it starts the separate routing request. If routing fails, the report stays approved even though no jurisdiction received it, which can advance the reporter’s verification progress and remove the report from the state requiring routing. Make this one atomic server operation, or compensate by reversing approval when routing cannot complete. This must be addressed before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines 123 to +126
export function useReportChatHistory(id: string | null) {
return useQuery<ChatHistoryResponse>({
queryKey: queryKeys.reports.chat(id ?? ""),
queryFn: () => api.reportMessages({ id: id as string, limit: REPORT_CHAT_LIMIT }),
queryFn: () => api.adminReportMessages({ id: id as string, limit: REPORT_CHAT_LIMIT }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Load older chat messages

This request loads only the newest 50 messages and does not use the returned cursor. The discussion view offers no way to load older messages and presents the loaded-window length as the chat count, so operators cannot inspect or moderate earlier discussion on active reports. This is non-blocking, but it makes moderation incomplete and the displayed count misleading.

Artifacts

Evidence from the check

  • The authored Vitest test simulates a 70-message cursor-paginated chat response and asserts the client request and discussion UI behavior, with the takeaway that only the first 50-message page is accessible.

Command output from the check

  • Captured output from running the narrow Vitest client/UI test in the admin workspace; it reports the simulated 70-message conversation receives and counts only 50 with no older-page control, confirming the finding.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/reports/use-reports.ts
Line: 123-126

Comment:
**Load older chat messages**

This request loads only the newest 50 messages and does not use the returned cursor. The discussion view offers no way to load older messages and presents the loaded-window length as the chat count, so operators cannot inspect or moderate earlier discussion on active reports. This is non-blocking, but it makes moderation incomplete and the displayed count misleading.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

<span className="meta" title={LOADED_TITLE}>
{loadedCount(activeCount, !!activeListQuery.hasNextPage)}
</span>
<span className="meta">{activeCount}</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Clarify loaded mail count

The header displays the number of loaded mail rows as a bare total while “Load more” remains available. Operators can read that partial page length as the full queue size. This is non-blocking, but it gives an inaccurate view of outstanding mail; retain a “+” or “loaded so far” treatment, or show a server-provided total.

Artifacts

Mail pagination browser fixture

  • The authored browser fixture simulates a paginated Mail response and captures the count before and after the second page loads.

Mail pagination browser output

  • The rendered-state output shows count 2 with Load more available, then count 3 after the final page loads.

▶ Mail count before loading another page

  • The recording shows a bare count of 2 while another Mail page remains available.

▶ Mail count after loading another page

  • The recording shows the count changing to 3 after the remaining Mail page loads.

Mail pagination fixture server output

  • The server output records the local rendered fixture used for the paginated Mail interaction.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/mail/mail-page.tsx
Line: 708

Comment:
**Clarify loaded mail count**

The header displays the number of loaded mail rows as a bare total while “Load more” remains available. Operators can read that partial page length as the full queue size. This is non-blocking, but it gives an inaccurate view of outstanding mail; retain a “+” or “loaded so far” treatment, or show a server-provided total.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment thread apps/admin/package.json
},
"dependencies": {
"@civfix/shared": "^0.52.0",
"@civfix/shared": "^0.53.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Use the changeset workflow

This directly changes the @civfix/shared version range and refreshes the lockfile without adding a changeset. The repository directive requires dependency version changes to use the changeset workflow. This repository requirement must be satisfied before merging.

Rule Used: # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for correctness, security and performance. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/package.json
Line: 18

Comment:
**Use the changeset workflow**

This directly changes the `@civfix/shared` version range and refreshes the lockfile without adding a changeset. The repository directive requires dependency version changes to use the changeset workflow. This repository requirement must be satisfied before merging.

**Rule Used:** # civfix review rules  civfix is a live civic-tech platform that will hold government contracts. Review every PR for **correctness, security and performance**. Flag real defects with evidence; skip style nits that lint already covers.  ## Repos  - **... ([source](https://app.greptile.com/civfix/-/custom-context?memory=39a53925-3d93-4c82-980e-27b67393717d))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +8 to +12
/**
* The moderation tile leads with the server-side queue total when the summary carries one. When the
* field is absent (an older API) it leads with a plain label instead of the length of the two-row
* preview, which is not a count of anything.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Remove new explanatory comments

This change adds a new explanatory block comment. The repository directive says new code must not add comments; the same new-comment pattern also appears in the government-claim presentation helpers and on SectionSummary.lead. Keep the implementation self-explanatory and remove the new comments. This repository requirement must be satisfied before merging.

Rule Used: # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for correctness, security and performance. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/features/home/home-preview-presentation.ts
Line: 8-12

Comment:
**Remove new explanatory comments**

This change adds a new explanatory block comment. The repository directive says new code must not add comments; the same new-comment pattern also appears in the government-claim presentation helpers and on `SectionSummary.lead`. Keep the implementation self-explanatory and remove the new comments. This repository requirement must be satisfied before merging.

**Rule Used:** # civfix review rules  civfix is a live civic-tech platform that will hold government contracts. Review every PR for **correctness, security and performance**. Flag real defects with evidence; skip style nits that lint already covers.  ## Repos  - **... ([source](https://app.greptile.com/civfix/-/custom-context?memory=39a53925-3d93-4c82-980e-27b67393717d))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +93 to +100
--cat-trash: #776C60;
--cat-recycling: #63A45A;
--cat-graffiti: #9B7ED9;
--cat-hazard: #E4574A;
--cat-encampment: #3E9E8E;
--cat-water: #74A9D8;
--cat-other: #8D8577;
--cat-cleanup: #D9A21B;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Use category color tokens

This change replaces the cleanup category token reference with a raw hex value while retaining a literal category palette for changed presentation. The frontend styling directive requires design tokens and prohibits new raw hex colors. Source the category colors from the shared category-color mechanism or existing semantic variables. This repository requirement must be satisfied before merging.

Rule Used: # civfix review rules civfix is a live civic-tech platform that will hold government contracts. Review every PR for correctness, security and performance. Flag real defects with evidence; skip style nits that lint already covers. ## Repos - **... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/admin/src/styles/colors-and-type.css
Line: 93-100

Comment:
**Use category color tokens**

This change replaces the cleanup category token reference with a raw hex value while retaining a literal category palette for changed presentation. The frontend styling directive requires design tokens and prohibits new raw hex colors. Source the category colors from the shared category-color mechanism or existing semantic variables. This repository requirement must be satisfied before merging.

**Rule Used:** # civfix review rules  civfix is a live civic-tech platform that will hold government contracts. Review every PR for **correctness, security and performance**. Flag real defects with evidence; skip style nits that lint already covers.  ## Repos  - **... ([source](https://app.greptile.com/civfix/-/custom-context?memory=39a53925-3d93-4c82-980e-27b67393717d))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@greptile-apps

greptile-apps Bot commented Sep 22, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.

  • P2 Admin report chat hides older cursor-paginated messages and reports a partial count ▶

    • Bug
      • With 70 simulated messages, the first API response supplies 50 newest messages and a cursor for the remaining 20. The client makes only the first request, never follows that cursor, renders no older-message control, and bases the Chat count on the 50 loaded items. The 20 older messages are inaccessible from this view and the count is partial.
    • Cause
      • useReportChatHistory uses a single useQuery with a fixed limit: 50 and ignores ChatHistoryResponse.nextCursor; ReportDiscussion renders items.length and has no pagination UI.
    • Fix
      • Use a cursor-aware infinite query (or explicit older-page state) that passes the returned cursor to api.adminReportMessages, merges loaded pages, exposes a load-older control while a cursor remains, and label/count the conversation accurately (or fetch all pages when an exact count is required).

@theobong
theobong merged commit 95860bb into main Sep 22, 2026
1 of 2 checks passed
@theobong
theobong deleted the feat/admin-city-comms branch September 22, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant