feat(rust): chat titles, BetterChat group styles, the voice and popups (phase 17)
PLAN.md §33, D134-D143. Protocol 12. - Chat titles (D135-D137): per-server rules (stat, top N, text, colour) that rank the current wipe, and a mode (first | all | up to N). Worked out once in model/titles and read three ways: pushed whole to the game by a new titleSync loop (on change, restart or wipe), and on every leaderboard row as `titles`. Admin: PUT /servers/:id/titles. - Group styles (D138, D139): a site group may carry all twelve BetterChat fields (rust_perm_group_chat). They ride perm.sync with `expect` from the pushed ledger, which gains a value column; a field changed in game is a `chat-field` drift row with the game's value, adopted into the style or put back. A withdrawn style is one `chat-group` retirement, never for `default`, cleared from the ledger only once BetterChat removed it. - The voice (D140): one fleet setting naming a styled group; news and rust.announce chat lines carry its format and the plugin says them with no sender. Admin: GET/PUT /voice. - Popups (D141, D142): rust.announce gains `delivery` (still version 1, from rust.options.delivery); each server gains news_delivery beside the news switch; `popup-unavailable` is not retried. - GET /servers/:id/integrations reads, live, which optional mods a server has loaded. README lists BetterChat and PopupNotifications as optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -20,12 +20,16 @@
|
||||
//
|
||||
// Test and Delete act at once rather than on Save — they are questions put to a
|
||||
// sidecar and a removal, not settings.
|
||||
//
|
||||
// Phase 17 added a server's chat titles and the announcement voice, in
|
||||
// `ChatTitles.jsx`, each saved on its own.
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ErrorState, Loading, useAsync } from '../../core.js'
|
||||
import api from '../../api.js'
|
||||
import { ago, nextWipe } from '../../lib/format.js'
|
||||
import { TitlesForm, VoiceCard, integrationsLine, titlesSummary } from './ChatTitles.jsx'
|
||||
|
||||
const RULES = [
|
||||
{ id: 'none', label: 'No schedule', hint: 'Nothing is forecast, not even the monthly forced wipe.' },
|
||||
@@ -139,6 +143,7 @@ export default function ServerSettings() {
|
||||
const [error, setError] = useState('')
|
||||
const [notes, setNotes] = useState({})
|
||||
const [confirming, setConfirming] = useState(null)
|
||||
const [titling, setTitling] = useState(null)
|
||||
|
||||
if (loadError) return <ErrorState error={loadError} />
|
||||
if (!data) return <Loading />
|
||||
@@ -178,6 +183,16 @@ export default function ServerSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
const mods = async (server) => {
|
||||
setNotes((n) => ({ ...n, [server.id]: 'Asking the game…' }))
|
||||
try {
|
||||
const line = integrationsLine(await api.admin.integrations(server.id))
|
||||
setNotes((n) => ({ ...n, [server.id]: line }))
|
||||
} catch (err) {
|
||||
setNotes((n) => ({ ...n, [server.id]: err.message || 'The game could not be asked.' }))
|
||||
}
|
||||
}
|
||||
|
||||
const remove = async (server) => {
|
||||
setConfirming(null)
|
||||
try {
|
||||
@@ -219,9 +234,12 @@ export default function ServerSettings() {
|
||||
? `Next wipe ${nextWipe(s.nextWipe)}, from ${SOURCE[s.nextWipe.source] || s.nextWipe.source}.`
|
||||
: 'No wipe schedule set.'}
|
||||
</div>
|
||||
<div className="dim" style={{ fontSize: '0.76rem', marginTop: 2 }}>{titlesSummary(s.titles, s.titlePush)}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 8 }}>
|
||||
<button type="button" className="btn" onClick={() => { setError(''); setForm(formFrom(s)) }}>Edit</button>
|
||||
<button type="button" className="btn" onClick={() => { setError(''); setTitling(null); setForm(formFrom(s)) }}>Edit</button>
|
||||
<button type="button" className="btn" onClick={() => test(s)}>Test the sidecar</button>
|
||||
<button type="button" className="btn" onClick={() => { setForm(null); setTitling(s) }}>Chat titles</button>
|
||||
<button type="button" className="btn" onClick={() => mods(s)}>Optional mods</button>
|
||||
{confirming === s.id ? (
|
||||
<>
|
||||
<button type="button" className="btn" onClick={() => remove(s)}>Delete {s.name} and everything recorded about it</button>
|
||||
@@ -244,6 +262,17 @@ export default function ServerSettings() {
|
||||
{form && (
|
||||
<ServerForm form={form} set={set} busy={busy} error={error} onSave={save} onCancel={() => setForm(null)} />
|
||||
)}
|
||||
|
||||
{titling && (
|
||||
<TitlesForm
|
||||
key={titling.id}
|
||||
server={titling}
|
||||
onSaved={() => { setTitling(null); setReloads((n) => n + 1) }}
|
||||
onCancel={() => setTitling(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<VoiceCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user