feat(rust): chat titles rank twenty-three conditions, and admins name the categories (PLAN_REDESIGNS §5, D172-D175, D209)

- Schema: fourteen title columns on rust_player_wipe_stats (the two
  best_* distances move by GREATEST, the rest are sums), rust_weapon_kills
  (kills by weapon prefab name) and rust_title_categories (the admin's
  title per category). purge.sql drops the two tables.
- Ingest: player.tally's new fields, in one statement per frame, none
  for an older plugin's frame.
- Titles: 23 categories plus playtime, each naming where it is read
  from (a sum, a MAX, npc_kills - animal_kills in signed arithmetic, or
  a named list). The bow/melee/blade/revolver/wood/ore/plants lists are
  one file, applied when a title is read. "NPC kills" ranks human NPCs
  only (D209).
- A rule's text may be empty, meaning the category's title: the rule's
  own, then the admin's, then the default. Typed-only-markup is still
  refused. A rename forgets every server's cached answer.
- Admin API: GET /admin/rust/title-categories and
  PUT /admin/rust/title-categories/:stat (empty text resets); the server
  list returns them too. Swagger fragment and routes.manifest.json
  regenerated (63 routes, against the pinned core).
- Screen: every category in the rule form, the category's title as a
  placeholder, and a Category titles section with Save and Reset.

The weapon lists are unverified until the rig probe runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-28 23:32:59 -05:00
parent afb16112b0
commit aea94b5d7a
18 changed files with 1150 additions and 65 deletions

View File

@@ -29,7 +29,7 @@ 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'
import { CategoryTitles, TitlesForm, VoiceCard, integrationsLine, titlesSummary } from './ChatTitles.jsx'
const RULES = [
{ id: 'none', label: 'No schedule', hint: 'Nothing is forecast, not even the monthly forced wipe.' },
@@ -164,6 +164,7 @@ export default function ServerSettings() {
if (loadError) return <ErrorState error={loadError} />
if (!data) return <Loading />
const servers = data.servers || []
const categories = data.titleCategories || []
const set = (key) => (e) => {
const value = e && e.target ? (e.target.type === 'checkbox' ? e.target.checked : e.target.value) : e
@@ -250,7 +251,7 @@ 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 className="dim" style={{ fontSize: '0.76rem', marginTop: 2 }}>{titlesSummary(s.titles, s.titlePush, categories)}</div>
{zoneHelperNote(s.zoneHelper) && (
<div style={{ fontSize: '0.76rem', marginTop: 2, color: 'var(--warn, #d9c184)' }}>{zoneHelperNote(s.zoneHelper)}</div>
)}
@@ -286,11 +287,14 @@ export default function ServerSettings() {
<TitlesForm
key={titling.id}
server={titling}
categories={categories}
onSaved={() => { setTitling(null); setReloads((n) => n + 1) }}
onCancel={() => setTitling(null)}
/>
)}
<CategoryTitles categories={categories} onSaved={() => setReloads((n) => n + 1)} />
<VoiceCard />
</div>
)