Add player portal + character-sheet front end (phase 4 follow-up)

Turns the raw shard endpoints into proper, navigable pages in the site's visual
language.

- components/CharacterSheet.jsx: reusable sheet — attribute tiles, vitals bars,
  resistances, skills (with bars), and equipment — styled with the shared
  panel/grid vocabulary.
- Player portal with a nav bar: PlayerPortalLayout (Characters / Account tabs +
  sign-out) wraps /player and /account. /player (PlayerCharacters) tells the
  logged-in player if they haven't linked a game account (with the [link code
  prompt) or, once linked, shows their characters grouped by account; each
  character opens its sheet at /player/char/:serial. Account security moved into
  the same shell (the buried "Game accounts" block was removed from it).
  Login/register now land on /player.
- Public: GET /public/shard/online (redacted name+serial+map) drives an
  "Online now" list on /site/shard that links to public character sheets at
  /site/shard/char/:serial (ShardChar). Swagger: ShardOnlinePlayer + regenerated.
- api.shard.online added.

Verified live against the running shard: Darrow's full sheet (STR 120, 58
skills, 3 equipment) renders through the browser-facing proxy; the online list
returns the live roster; player routes 401 without a session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3
This commit is contained in:
2026-07-11 02:51:50 -05:00
parent e7bc316863
commit fe6f93481b
15 changed files with 609 additions and 218 deletions

View File

@@ -162,6 +162,13 @@ publicRouter.get(
validate,
shard.getEconomy,
)
publicRouter.get(
'/shard/online',
// #swagger.tags = ['Public · Shard']
// #swagger.summary = 'Players online now (name + serial + map only)'
/* #swagger.responses[200] = { description: 'Online players', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardOnlinePlayer" } } } } } */
shard.getOnline,
)
publicRouter.get(
'/shard/idoc',
// #swagger.tags = ['Public · Shard']

View File

@@ -69,6 +69,19 @@ async function getEconomy(req, res) {
}
}
// GET /public/shard/online — who is online now (redacted: name + serial + map,
// no coordinates, vitals or account). Feeds the public "online now" list, which
// links to the public character sheet.
async function getOnline(req, res) {
try {
const rows = await shardState.listOnline()
return res.json(rows.map((r) => ({ serial: r.serial, name: r.name, map: r.map })))
} catch (err) {
log.error('shard.getOnline', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// GET /public/shard/idoc — houses currently in danger (stage IDOC).
async function getIdoc(req, res) {
try {
@@ -118,4 +131,4 @@ function stream(req, res) {
broadcast.subscribe(req, res, 'public')
}
module.exports = { getStatus, getFeed, getEconomy, getIdoc, getChar, stream }
module.exports = { getStatus, getFeed, getEconomy, getOnline, getIdoc, getChar, stream }

View File

@@ -1410,6 +1410,33 @@
}
}
},
"/api/v1/public/shard/online": {
"get": {
"tags": [
"Public · Shard"
],
"summary": "Players online now (name + serial + map only)",
"description": "",
"responses": {
"200": {
"description": "Online players",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ShardOnlinePlayer"
}
}
}
}
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/public/shard/idoc": {
"get": {
"tags": [
@@ -10307,6 +10334,67 @@
}
}
},
"ShardOnlinePlayer": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "object"
},
"description": {
"type": "string",
"example": "A player online now (redacted for the public list)."
},
"properties": {
"type": "object",
"properties": {
"serial": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "0x24C"
}
}
},
"name": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"example": {
"type": "string",
"example": "Darrow"
}
}
},
"map": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "string"
},
"nullable": {
"type": "boolean",
"example": true
},
"example": {
"type": "string",
"example": "Trammel"
}
}
}
}
}
}
},
"ShardHouse": {
"type": "object",
"properties": {

View File

@@ -544,6 +544,15 @@ const doc = {
t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 },
},
},
ShardOnlinePlayer: {
type: 'object',
description: 'A player online now (redacted for the public list).',
properties: {
serial: { type: 'string', example: '0x24C' },
name: { type: 'string', example: 'Darrow' },
map: { type: 'string', nullable: true, example: 'Trammel' },
},
},
ShardHouse: {
type: 'object',
description: 'A house at its current decay stage.',