// Public · Teams — the anonymous read surface (TEAMS.md §2.11). // // Every handler here is a projection over core's own tables; nothing calls the // module. A Team page must render while the shard is down, showing a roster // marked stale, because that is what the projection is for. const teams = require('../../../model/teams/teams.model') const teamActivity = require('../../../model/teams/teamActivity.model') const log = require('../../../utils/logger')('teams') const fail = (res, err, what) => { log.error(`public teams: ${what} failed`, { message: err.message }) return res.status(500).json({ message: 'Internal Server Error' }) } async function listTeams(req, res) { try { const limit = Math.min(Number.parseInt(req.query.limit, 10) || 50, 200) const offset = Math.max(Number.parseInt(req.query.offset, 10) || 0, 0) return res.json(await teams.listPublic({ limit, offset })) } catch (err) { return fail(res, err, 'list') } } async function getTeam(req, res) { try { const team = await teams.getPublic(req.params.slug) // A hidden Team is indistinguishable from a missing one here, deliberately: // "absent from every public surface" includes not confirming it exists. if (!team) return res.status(404).json({ message: 'Team not found' }) return res.json(team) } catch (err) { return fail(res, err, 'get') } } /** * The roster, projected for whoever is asking (§3.3). * * The viewer is described to the module rather than handed over: it gets the * caller's id and role, which is what a rung decision turns on, and not the user * row — a module has `ctx.users.getById` if it needs more, and passing the whole * record here would make every column of `users` part of this contract. */ async function getRoster(req, res) { try { const viewer = req.user ? { userId: req.user.id, role: req.user.role } : null const roster = await teams.rosterPublic(req.params.slug, viewer) if (!roster) return res.status(404).json({ message: 'Team not found' }) return res.json(roster) } catch (err) { return fail(res, err, 'roster') } } /** * A Team's activity feed (§4.3). * * The only handler in this tier that reads `req.user`, and it reads nothing else * from the caller about what they may see: `limit` and `offset` are page * controls, and the visibility filter is resolved from the session alone. A * request parameter naming its own visibility is the bug the ENUM exists to * prevent, so there is deliberately no way to ask for one. * * The cap is 100 rather than the index's 200 — every row carries a summary and an * opaque payload, so a page of these is much larger than a page of Teams. */ async function getActivity(req, res) { try { const limit = Math.min(Math.max(Number.parseInt(req.query.limit, 10) || 50, 1), 100) const offset = Math.max(Number.parseInt(req.query.offset, 10) || 0, 0) const feed = await teamActivity.feedFor(req.params.slug, req.user ? req.user.id : null, { limit, offset }) if (!feed) return res.status(404).json({ message: 'Team not found' }) return res.json(feed) } catch (err) { return fail(res, err, 'activity') } } module.exports = { listTeams, getTeam, getRoster, getActivity }