Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions apps/backoffice/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ burger menu (see [The menu](#the-menu)), by group:
[Who sent a note](#who-sent-a-note)) and its photos and videos (see
[A note's photos and videos](#a-notes-photos-and-videos)).
- **Moderation**: Reports, which the operator acts on and closes (see [Acting on a report](#acting-on-a-report)),
and Players, who played and every tool on one player (see [The player page](#the-player-page)).
Drawings, the reports of a drawing on the map and the tools that undo one (see [Drawings](#drawings)), and
Players, who played and every tool on one player (see [The player page](#the-player-page)).
- **Map**: Reassign a country, Paint random tiles, Download a replay (see [The map tools](#the-map-tools)).
- **Ops**: Reconcile titles, the run after a deploy (see [Ops](#ops)).

What is left of the plan is a live chat view: `ListMessages` pages through the chat (the report page reads it),
and no admin method streams it.
What is left of the plan is a live chat view: `ListMessages` pages through the chat (the report and player pages
read it), and no admin method streams it.

It is a third app, not a page of the game: **it imports nothing from `apps/frontend`** and has no Three.js (see
the root `CLAUDE.md`, "Independence of the apps").
Expand Down Expand Up @@ -144,20 +145,21 @@ note and keeps its text).
the oldest or the newest message shown. A page shorter than asked is the end, and its button goes. The reported
message shows while they load, marked ("Reported message", `aria-current`), and stays shown if they fail.
`GetMessage` names no author, so its line takes the name the report gives its account.
- **What the reported account said, in its conversations** (`TheirMessages`, "What Ana said"), for every report: a
report from a player's card names no message, and one line cannot be judged alone. `ListMessages` with the
account's id reads its messages, 20 at a time (`THEIR_PAGE`), then `moreConversations` (`theirConversations.ts`)
reads the whole chat around each, `AROUND` (5) messages each side, and shows them at once, newest first, three at a
time (`CONVERSATIONS`), "Show more conversations" for the next. **One of their messages a conversation already holds
gets none of its own, and a conversation that reaches the next one joins it**, so an exchange of three insults is
one conversation, read with two calls. Their lines are marked (`line-theirs`) and tagged `reported`, the reporter's
`reporter`. Each conversation grows with "Show earlier" and "Show later". Nothing kept is "Nothing they said in the
chat is kept."
- **What the reported account said, in its conversations** (`chat/TheirMessages`, "What Ana said"), for every
report: a report from a player's card names no message, and one line cannot be judged alone. `ListMessages` with
the account's id reads its messages, 20 at a time (`THEIR_PAGE`), then `moreConversations`
(`theirConversations.ts`) reads the whole chat around each, `AROUND` (5) messages each side, and shows them at
once, newest first, three at a time (`CONVERSATIONS`), "Show more conversations" for the next. **One of their
messages a conversation already holds gets none of its own, and a conversation that reaches the next one joins
it**, so an exchange of three insults is one conversation, read with two calls. Their lines are marked
(`line-theirs`) and tagged `reported`, the reporter's `reporter`. Each conversation grows with "Show earlier" and
"Show later". Nothing kept is "Nothing they said in the chat is kept."
- **Each line** has its author's name as the chat names it now, its country and its time (with the date when it
is not the reported message's day), what it answers ("In reply to …"), and is struck through with when it was
removed. **What the reporter and the reported account said is tagged** `reporter` and `reported`, by account
id. An author's name opens it on the player page (`OpenPlayer`), so a third player in the conversation is a
click away. A page past the chat's retention is `not_found`: "That part of the chat is no longer kept."
click away; the player already open there stays open, with its row. A page past the chat's retention is
`not_found`: "That part of the chat is no longer kept."
- **The actions, all on the reported account id**, never its name: remove the message (`RemoveMessage`, only while
it is shown), mute (`Mute`, an hour by default), ban (`BanPlayer`, no duration by default, so the antibot's
ladder picks the next step) and inspect (`InspectPlayer`). Then close the report (`DecideReport`): Acted or
Expand Down Expand Up @@ -189,6 +191,30 @@ note and keeps its text).
each request (`askedFor`) and can refuse a method with a code. The jsdom tests drive each action through its
page.

## Drawings

`src/drawings/`: the reports of a drawing on the map (`ListPlaceReports`), a page like Reports.

- **The list** shows the open reports first, and a switch shows them all: the reason (hate, sexual, other), the
tile, the reporter's name, the text and the time, and the decision once decided.
- **The detail** links the drawing on the map, in a new tab (`mapLinkOf`: the game at `?tile=<id>`, close up on
the tile), and holds the area every tool acts on: the reported tile and the rings around it, 8 by default, as a
bomb's blast, 1 to 50.
- **Restore** (`RestoreArea`, `RestorePanel`) puts each tile of the area back to what it held at a time: an hour
before the report by default (`beforeOf`). The dry run says how many tiles the area holds and how many changed
since, how far back the ledger reaches (`kept_since`, then the field's `min`), and who painted the area since,
most takes first, with their takes and tiles in the area alone: each by flag and name (`GetAuthors`, as the
players list), each opening on the player page, where the ban is (`PainterList`). **Restore stays off until a dry
run matches the rings and the time**, asks with its count ("Put back the 12 tiles changed since …?"), and needs a
new dry run after it. A time older than the ledger is `failed_precondition`: "The ledger does not reach back to
then: clear the area instead."
- **Clear** (`ClearArea`, `ClearPanel`) gives every tile of the area that a flag holds to nobody, for a drawing older
than the ledger. Dry run first (how many are held), then Clear, which asks with its count, on the same rule.
- **Close the report**: Acted or Dismissed, with a comment, `DecidePlaceReport` through the same `DecideForm` as a
player's report (it takes the call that decides).
- `FakeAdmin` refuses a restore older than its `keptSince` and changes tiles only when it is not a dry run, as the
server does.

## The player page

`src/players/`. **A player is an account or an address** (`Caller` in `caller.ts`), never both: the planet's tools
Expand Down Expand Up @@ -218,6 +244,10 @@ address over IPv4, the /64 over IPv6) and checks.
heading is its flag and its name: the name the profile answers, which is the name it holds now, over the one the
page that opened it knew; the flag of its row, or else the country it took the most tiles for. `not_found` is an
account the game keeps nothing about. An address has no profile, and is not asked.
- **An account's page shows what it said, in its conversations** (`chat/TheirMessages`, as a report does): its
messages by its account id, the chat around each, three conversations at a time. Its lines are marked
(`line-theirs`), and no line is tagged: there is no report. An address has no messages, since the chat keeps them
by account, and is not asked.
- **The page inspects on open** (`InspectPlayer`): the antibot's readings, its ban, its pardon and its clicks. The antibot off
reads as such, not as a failure. A ban or an unban reads it again.
- **The tools**: ban (`BanPlayer`, the ladder's next step by default), unban (`UnbanPlayer`, which forgets the
Expand Down Expand Up @@ -289,8 +319,10 @@ src/
components/ Menu, ConfirmAction, FailureNotice, AccountId and Copyable (with Copy), Country, CountryField,
Fact, Person, When
support/ Ideas and bugs: NoteList, Attachments, labels.ts (names, outcomes, device lines, sizes)
reports/ Reports: list, detail, the report's own actions, Conversation and TheirMessages over
chatPages.ts (a page of ListMessages, the parties a report names) and theirConversations.ts
chat/ Conversation and TheirMessages, which reports/ and players/ show, over chatPages.ts (a page of
ListMessages, the parties a report names) and theirConversations.ts
reports/ Reports: list, detail, the report's own actions, DecideForm
drawings/ Drawings: list, detail, RestorePanel, ClearPanel, PainterList, actions.ts
players/ Players: list, PlayerName, PlayerView, ProfilePanel, the tools on one player, actions.ts
map/ Reassign a country, Paint random tiles, Download a replay
ops/ Reconcile titles
Expand Down
7 changes: 5 additions & 2 deletions apps/backoffice/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {ReassignPanel} from "./map/ReassignPanel.tsx"
import {ReplayPanel} from "./map/ReplayPanel.tsx"
import {OpenPlayer, Opened, PageId, titleOf} from "./navigation.ts"
import {TitlesPanel} from "./ops/TitlesPanel.tsx"
import {idOf} from "./players/caller.ts"
import {PlayersPage} from "./players/PlayersPage.tsx"
import {ReportsView} from "./reports/ReportsView.tsx"
import {DrawingsView} from "./drawings/DrawingsView.tsx"
import {Callback} from "./signIn/callback.ts"
import {SignInCallback} from "./signIn/SignInCallback.tsx"
import {SignOutButton} from "./signIn/SignOutButton.tsx"
Expand Down Expand Up @@ -47,9 +49,9 @@ function Console({operator, onSignedOut}: {operator: string, onSignedOut: () =>
const [page, setPage] = usePage()
const [player, setPlayer] = useState<Opened | undefined>(undefined)

// A report opens its player on the Players page, where every tool on it is.
// A report opens its player on the Players page, where every tool on it is. The player already open keeps its row.
const openPlayer = useCallback((opened: Opened) => {
setPlayer(opened)
setPlayer(shown => shown !== undefined && idOf(shown.caller) === idOf(opened.caller) ? shown : opened)
setPage("players")
}, [setPage])

Expand All @@ -73,6 +75,7 @@ function Console({operator, onSignedOut}: {operator: string, onSignedOut: () =>
<h2 className="page-title">{titleOf(page)}</h2>
{page === "notes" && <NoteList/>}
{page === "reports" && <ReportsView/>}
{page === "drawings" && <DrawingsView/>}
{page === "players" && <PlayersPage opened={player} onOpen={setPlayer}/>}
{page === "reassign" && <ReassignPanel planet={CLIENTS.planet}/>}
{page === "paint" && <PaintPanel planet={CLIENTS.planet}/>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ type Props = {
chat: ChatAdmin
anchor: KeptMessage
removedAt: Date | undefined
parties: Parties
// What the anchor is to the operator: "Reported message", "Their message".
parties: Parties | undefined
// What the anchor is to the operator: "Reported message", "This message".
mark: string
}

Expand Down Expand Up @@ -55,7 +55,7 @@ type PagesProps = {
chat: ChatAdmin
first: {before: Page, after: Page}
reported: Reported
parties: Parties
parties: Parties | undefined
}

function Pages({chat, first, reported, parties}: PagesProps) {
Expand Down Expand Up @@ -118,7 +118,7 @@ type LinesProps = {
before: KeptMessage[]
reported: Reported
after: KeptMessage[]
parties: Parties
parties: Parties | undefined
}

function Lines({before, reported, after, parties}: LinesProps) {
Expand All @@ -134,7 +134,7 @@ function Lines({before, reported, after, parties}: LinesProps) {

type LineProps = {
line: KeptMessage
// Who the report names, to tag what each said; none when every line is one party's.
// Who the report names, to tag what each said; none off a report, or when every line is one party's.
parties: Parties | undefined
// The anchor's day: a line sent on another one shows its date too, as the anchor does.
day: Date | undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Props = {
chat: ChatAdmin
accountId: string
name: string
parties: Parties
parties: Parties | undefined
}

type Built = {
Expand All @@ -34,8 +34,8 @@ async function theirPage(chat: ChatAdmin, accountId: string, before: string, sig
return {newestFirst: [...page.messages].reverse(), more: page.more, oldest: page.messages[0]?.messageId ?? before}
}

// What the reported account said in the chat, in the conversations it said it in, whatever the report names: a
// report from a player's card names no message.
// What one account said in the chat, in the conversations it said it in, by its account id: a report from a
// player's card names no message.
export function TheirMessages({chat, accountId, name, parties}: Props) {
const read = useCallback<Read>((ask, limit) => pageOf(chat, ask, limit), [chat])
const first = useCallback(async (signal: AbortSignal): Promise<Built> => {
Expand All @@ -57,7 +57,7 @@ export function TheirMessages({chat, accountId, name, parties}: Props) {
)
}

type ConversationsProps = {chat: ChatAdmin, accountId: string, first: Built, read: Read, parties: Parties}
type ConversationsProps = {chat: ChatAdmin, accountId: string, first: Built, read: Read, parties: Parties | undefined}

function Conversations({chat, accountId, first, read, parties}: ConversationsProps) {
const [built, setBuilt] = useState(first)
Expand Down Expand Up @@ -101,7 +101,7 @@ type WindowProps = {
chat: ChatAdmin
window: Window
accountId: string
parties: Parties
parties: Parties | undefined
onGrown: (window: Window) => void
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ChatAdmin} from "../clients.ts"
import {GetMessageResponse, KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"
import {KeptMessage} from "../gen/grpc/chat/v1/admin_pb.ts"

// An account a report names, so the conversation marks what it said.
export type Party = {accountId: string, name: string}
Expand All @@ -13,17 +13,3 @@ export async function pageOf(chat: ChatAdmin, ask: Ask, limit: number, signal?:
const answer = await chat.listMessages({...ask, limit}, {signal})
return {messages: answer.messages, more: answer.messages.length === limit}
}

// GetMessage names no author: the report does, for the account it names.
export function keptOf(message: GetMessageResponse, parties: Parties): KeptMessage {
const party = [parties.reported, parties.reporter].find(each => each.accountId === message.accountId)
return new KeptMessage({
messageId: message.messageId,
accountId: message.accountId,
authorName: message.accountId === "" ? "" : party?.name ?? "",
text: message.text,
sentAt: message.sentAt,
countryId: message.countryId,
removedAt: message.removedAt,
})
}
5 changes: 4 additions & 1 deletion apps/backoffice/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {AdminService as SupportAdminService} from "./gen/grpc/support/v1/admin_c
export type ChatAdmin = Pick<PromiseClient<typeof ChatAdminService>, "getMessage" | "listMessages" | "removeMessage" | "mute">
export type PlanetAdmin = PromiseClient<typeof PlanetAdminService>
export type PlayerAdmin = PromiseClient<typeof PlayerAdminService>
export type SupportAdmin = Pick<PromiseClient<typeof SupportAdminService>, "listNotes" | "decideReport">
export type SupportAdmin = Pick<
PromiseClient<typeof SupportAdminService>,
"listNotes" | "decideReport" | "listPlaceReports" | "decidePlaceReport"
>

export type Clients = {chat: ChatAdmin, planet: PlanetAdmin, player: PlayerAdmin, support: SupportAdmin}

Expand Down
60 changes: 60 additions & 0 deletions apps/backoffice/src/drawings/ClearPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {useState} from "react"
import {PlanetAdmin} from "../clients.ts"
import {ConfirmAction} from "../components/ConfirmAction.tsx"
import {countOf} from "../support/labels.ts"
import {useAction} from "../useAction.ts"
import {clearArea, Cleared, refusalOf} from "./actions.ts"

type Props = {
planet: PlanetAdmin
tileId: number
rings: number | undefined
}

const refused = (err: unknown) => refusalOf("clear", err)

// For a drawing older than the ledger keeps, which no restore reaches: every tile of the area goes to nobody.
export function ClearPanel({planet, tileId, rings}: Props) {
const [count, start, reset] = useAction<Cleared>(refused)
const [countedFor, setCountedFor] = useState<number | undefined>(undefined)
const counted = count.step === "done" && countedFor === rings ? count.value : undefined

const dryRun = () => {
if (rings === undefined) return
setCountedFor(rings)
start(() => clearArea(planet, tileId, rings, true))
}

return (
<section className="panel" aria-label="Clear">
<h2>Clear the area</h2>
<div className="action">
<button
type="button"
className="button"
disabled={count.step === "running" || rings === undefined}
onClick={dryRun}
>
{count.step === "done" ? "Dry run again" : "Dry run"}
</button>
{count.step === "refused" && <p className="action-refused" role="alert">{count.words}</p>}
{counted !== undefined && (
<p className="action-said" role="status">
{countOf(counted.tiles, "tile")} in the area, {counted.owned} held by a flag.
</p>
)}
</div>
<ConfirmAction
label="Clear"
tone="danger"
disabled={counted === undefined || counted.owned === 0}
question={`Give the ${countOf(counted?.owned ?? 0, "tile")} held in the area to nobody?`}
run={() => clearArea(planet, tileId, rings ?? 0, false)}
said={done => `Cleared ${done.cleared} of ${countOf(done.owned, "tile")} held.`
+ (done.cleared < done.owned ? " The rest were taken again while it ran." : "")}
refusal={refused}
onDone={reset}
/>
</section>
)
}
Loading