Add a "View" action beside Edit in the users table that opens a dedicated,
read-only page showing everything the uo-link shard knows about a user,
scoped to their linked game accounts: character rosters, currently-online
characters, houses (IDOC-first), and recent vendor sales.
Backend (admin-only, under the existing /users adminOnly gate):
- GET /admin/users/:id — single sanitized user (page is deep-linkable)
- GET /admin/users/:id/shard/{accounts,sales,houses,online}
- shardState: listHousesByAccounts / listOnlineByAccounts (+ model shapers)
- Extract salesForAccounts into utils/shardSales; reuse in player getSales
- Live rosters reuse the existing admin-bypass /admin/shard/* endpoints,
so no new routes for roster/vendors/char
Frontend:
- UserDetail page reusing CharacterStats / GameAccounts / VendorSales
- GameAccounts gains a readOnly prop (drops link form + self-voice copy)
- api.admin.getUser + api.admin.userShard(id) scope; route + layout title
Tests: adminUserShard.test.js (404, account scoping, empty accounts,
salesForAccounts cap/filter). Full server suite 164 pass; client builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
27 lines
1.0 KiB
JavaScript
27 lines
1.0 KiB
JavaScript
// Recent player-vendor sales for a set of game accounts. Shared by the player
|
|
// self endpoint (the caller's linked accounts) and the admin user-detail
|
|
// endpoint (a target user's linked accounts). Reads the site's own ingested
|
|
// event log — no sidecar round-trip — and filters to sales whose owning account
|
|
// is in the set. Newest 50, already newest-first from shardEvents.list.
|
|
|
|
const shardEvents = require('../model/shardEvents/shardEvents.model')
|
|
|
|
async function salesForAccounts(accounts) {
|
|
const set = accounts instanceof Set ? accounts : new Set(accounts)
|
|
if (set.size === 0) return []
|
|
const events = await shardEvents.list({ kind: 'vendor.sale', limit: 500 })
|
|
return events
|
|
.filter((e) => e.payload && set.has(e.payload.ownerAcct))
|
|
.slice(0, 50)
|
|
.map((e) => ({
|
|
t: e.t,
|
|
itemType: e.payload.itemType,
|
|
amount: e.payload.amount,
|
|
price: e.payload.price,
|
|
commission: e.payload.commission,
|
|
ownerAcct: e.payload.ownerAcct,
|
|
}))
|
|
}
|
|
|
|
module.exports = { salesForAccounts }
|