Admin → Teams told an operator that an unsynced projection "is not a confirmed empty shard". Teams is a core primitive and core's word for the host is the game server; "shard" is Ultima Online's. The detail now reads "What is shown below does not confirm that the game server has no Teams." The matching test, and the four comments that said "shard" about the same state, follow it. Found while making runicgateway.com's core docs game-neutral (runicgateway.com#34), which had to paraphrase this line rather than quote it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
101 lines
3.9 KiB
JavaScript
101 lines
3.9 KiB
JavaScript
// What core's Team activity feed SAYS, separated from how it renders
|
|
// (docs/website/TEAMS.md §4.3).
|
|
//
|
|
// Core renders this feed into a slot a MODULE declares on its own page, because
|
|
// Teams is a contract primitive and not a surface: core owns the feed, its
|
|
// visibility rules and its wording; the module owns the page and the vocabulary
|
|
// around it. So this file is deliberately narrow — the roster and index
|
|
// presentation that once lived here went with the core Team pages, to whichever
|
|
// module renders them.
|
|
//
|
|
// Plain JS with tests, following lib/teamAdmin.js. Worth splitting for the same
|
|
// reason it was there: a feed that is filtered, or a projection that is stale,
|
|
// has to say so in words, and getting that wording right is logic rather than
|
|
// markup.
|
|
|
|
const MINUTE = 60_000
|
|
const HOUR = 60 * MINUTE
|
|
const DAY = 24 * HOUR
|
|
|
|
/** "just now" / "14 minutes ago" / "3 hours ago" / "2 days ago". */
|
|
export function relativeTime(when, now = Date.now()) {
|
|
if (!when) return null
|
|
const ms = now - new Date(when).getTime()
|
|
if (!Number.isFinite(ms)) return null
|
|
if (ms < MINUTE) return 'just now'
|
|
if (ms < HOUR) {
|
|
const n = Math.floor(ms / MINUTE)
|
|
return `${n} ${n === 1 ? 'minute' : 'minutes'} ago`
|
|
}
|
|
if (ms < DAY) {
|
|
const n = Math.floor(ms / HOUR)
|
|
return `${n} ${n === 1 ? 'hour' : 'hours'} ago`
|
|
}
|
|
const n = Math.floor(ms / DAY)
|
|
return `${n} ${n === 1 ? 'day' : 'days'} ago`
|
|
}
|
|
|
|
/**
|
|
* How a public surface describes the projection's freshness (§2.4).
|
|
*
|
|
* Distinct from `teamAdmin.freshnessOf`, which is worded for an operator
|
|
* debugging a sync. A visitor needs one sentence about whether what they are
|
|
* looking at is current, and specifically must never be shown an unconfirmed
|
|
* empty projection as though it were a confirmed empty one.
|
|
*/
|
|
export function freshnessNote(sync = {}, now = Date.now()) {
|
|
// Nothing supplies Teams here, so there is nothing to be stale ABOUT. A
|
|
// deployment with no game module is not a broken one.
|
|
if (!sync.configured) return null
|
|
if (!sync.lastSyncAt) return { tone: 'warn', text: 'Not yet confirmed against the game.' }
|
|
const ago = relativeTime(sync.lastSyncAt, now)
|
|
if (sync.stale) return { tone: 'warn', text: `Last confirmed ${ago} — the game may have moved on.` }
|
|
return { tone: 'idle', text: `Last confirmed ${ago}.` }
|
|
}
|
|
|
|
/**
|
|
* Group feed items into days, newest first, preserving order within a day (§4.3).
|
|
*
|
|
* Keyed by local calendar date rather than by a UTC slice: "yesterday" is a
|
|
* property of where the reader is sitting, and a game server's evening raid landing at
|
|
* 00:30 UTC belongs on the day the players experienced it.
|
|
*/
|
|
export function groupByDay(items = [], locale = undefined) {
|
|
const days = []
|
|
const byKey = new Map()
|
|
for (const item of items) {
|
|
const date = new Date(item.occurredAt)
|
|
if (Number.isNaN(date.getTime())) continue
|
|
const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
|
|
if (!byKey.has(key)) {
|
|
const day = {
|
|
key,
|
|
label: date.toLocaleDateString(locale, { year: 'numeric', month: 'long', day: 'numeric' }),
|
|
items: [],
|
|
}
|
|
byKey.set(key, day)
|
|
days.push(day)
|
|
}
|
|
byKey.get(key).items.push(item)
|
|
}
|
|
return days
|
|
}
|
|
|
|
/**
|
|
* What to say under a feed that has been filtered.
|
|
*
|
|
* Only when there is something to say: a caller who saw everything is told
|
|
* nothing, and an anonymous caller is invited to sign in rather than simply
|
|
* informed that entries exist which they cannot have.
|
|
*
|
|
* The wording avoids core's own noun. The reader is looking at a page the module
|
|
* titled — a guild, a clan — and "this Team" would be core's vocabulary leaking
|
|
* onto a surface that deliberately does not use it.
|
|
*/
|
|
export function activityScopeNote(feed = {}, signedIn = false) {
|
|
if (feed.scope !== 'public') return null
|
|
return signedIn
|
|
? 'Some entries are visible to members only.'
|
|
: 'Sign in as a member to see the members-only entries.'
|
|
}
|