1
0
Fork 0

style: remove em-dashes from user-visible prose (wave 1)

This commit is contained in:
Jason Fraley 2026-09-07 01:34:28 -04:00
parent a96b5de52b
commit a2a5f7877b
94 changed files with 248 additions and 282 deletions

View file

@ -15,7 +15,7 @@ import { PreferencesForm } from "@/components/frontend/account/PreferencesForm";
import { RequestTransferDialog } from "@/components/frontend/account/RequestTransferDialog";
export const metadata = {
title: "Account — Polaris Task Force",
title: "Account (Polaris Task Force)",
};
function relationId(value: Assignment["leader"]): number | null {
@ -114,7 +114,9 @@ export default async function AccountPage() {
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex min-w-0 flex-col gap-1">
<CardTitle>Assignment</CardTitle>
<CardDescription>Review your current assignment and request a move.</CardDescription>
<CardDescription>
Review your current assignment and request a move.
</CardDescription>
</div>
<RequestTransferDialog
currentAssignments={currentAssignments}

View file

@ -7,7 +7,7 @@ import { Bomb } from "lucide-react";
import { EodArena } from "@/components/frontend/eod/EodArena";
export const metadata: Metadata = {
title: "Minefield Clearance — Polaris Task Force",
title: "Minefield Clearance (Polaris Task Force)",
description: "Clear simulated minefields across three EOD sectors to earn XP for the task force.",
};

View file

@ -7,7 +7,7 @@ import { Bird } from "lucide-react";
import { FlappyArena } from "@/components/frontend/flappy/FlappyArena";
export const metadata: Metadata = {
title: "Flight Simulator — Polaris Task Force",
title: "Flight Simulator: Polaris Task Force",
description: "Side training sim. Definitely not Flappy Bird.",
};

View file

@ -10,7 +10,7 @@ import Link from "next/link";
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
export const metadata = {
title: "Ticket — Polaris Task Force",
title: "Ticket: Polaris Task Force",
};
interface TicketPageProps {

View file

@ -133,7 +133,7 @@ export async function createTicket(input: {
userId: staffId,
type: "ticket:created",
title: "New ticket",
message: `${title} — ${reporterName}`,
message: `${title} by ${reporterName}`,
link: `/helpdesk/${createdTicket.id}`,
});
}
@ -233,7 +233,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
targetId: ticketId,
});
const replyMessage = `"${ticket.title}" — you have a new reply.`;
const replyMessage = `"${ticket.title}" has a new reply.`;
if (isStaff) {
if (reporterId != null && reporterId !== userId) {
await notifyUser(payload, {

View file

@ -9,7 +9,7 @@ import { LifeBuoyIcon } from "lucide-react";
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
export const metadata = {
title: "Helpdesk — Polaris Task Force",
title: "Helpdesk: Polaris Task Force",
};
export default async function HelpdeskPage() {

View file

@ -7,7 +7,7 @@ import type { Media } from "@/payload-types";
import { Button } from "@/components/ui/button";
export const metadata = {
title: "Campaigns — Polaris Task Force",
title: "Campaigns: Polaris Task Force",
};
export default async function CampaignsPage() {

View file

@ -5,7 +5,7 @@ import { ShieldIcon } from "lucide-react";
import { FactionCard } from "@/components/frontend/intelligence/FactionCard";
export const metadata = {
title: "Factions — Polaris Task Force",
title: "Factions | Polaris Task Force",
};
export default async function FactionsPage() {

View file

@ -430,7 +430,7 @@ export async function createFridaySideOp(
if (fridayConflict) {
return {
success: false,
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}) — only one operation per Friday, first come first served.`,
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}). Only one operation per Friday, first come first served.`,
};
}
@ -583,7 +583,7 @@ export async function updateFridaySideOp(
if (fridayConflict) {
return {
success: false,
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}) — only one operation per Friday, first come first served.`,
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}). Only one operation per Friday, first come first served.`,
};
}
}

View file

@ -5,7 +5,7 @@ import { CrosshairIcon } from "lucide-react";
import { MissionsList } from "@/components/frontend/intelligence/MissionsList";
export const metadata = {
title: "Missions — Polaris Task Force",
title: "Missions: Polaris Task Force",
};
export default async function MissionsPage() {

View file

@ -10,7 +10,7 @@ import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/com
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
export const metadata = {
title: "Intelligence — Polaris Task Force",
title: "Intelligence: Polaris Task Force",
};
export default async function IntelligenceOverviewPage() {

View file

@ -299,7 +299,7 @@ export async function moveLockerItem(
grid.height,
)
) {
return { success: false, error: "Cannot move here — overlaps with another item." };
return { success: false, error: "Cannot move here: overlaps with another item." };
}
items[idx] =

View file

@ -9,7 +9,7 @@ import { LockerView } from "@/components/frontend/locker/LockerView";
import { hasPermission } from "@/utils/access-control/hasPermission";
export const metadata = {
title: "Locker — Polaris Task Force",
title: "Locker (Polaris Task Force)",
};
export default async function LockerPage() {
@ -36,9 +36,9 @@ export default async function LockerPage() {
limit: 100,
depth: 1,
});
const loadouts = loadoutsRes.docs as unknown as Loadout[];
const loadouts = loadoutsRes.docs as unknown as Loadout[];
const isManager = user ? (await hasPermission(payload, user, "locker-storages:update")) : false;
const isManager = user ? await hasPermission(payload, user, "locker-storages:update") : false;
let assetsCatalog: Asset[] = [];
if (isManager) {

View file

@ -9,7 +9,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
import { getCurrencyConfig } from "@/lib/banking";
export const metadata = {
title: "Account — Polaris Task Force",
title: "Account: Polaris Task Force",
};
interface AccountPageProps {

View file

@ -10,7 +10,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
import { getCurrencyConfig } from "@/lib/banking";
export const metadata = {
title: "Banking — Polaris Task Force",
title: "Banking: Polaris Task Force",
};
export default async function BankingPage() {

View file

@ -13,7 +13,7 @@ import {
} from "@/components/ui/item";
export const metadata = {
title: "Vehicles — Polaris Task Force",
title: "Vehicles: Polaris Task Force",
};
export default async function GameVehiclesPage() {

View file

@ -47,7 +47,10 @@ async function authenticate() {
return { payload, user };
}
async function isMarketManager(payload: Awaited<ReturnType<typeof getPayload>>, user: User): Promise<boolean> {
async function isMarketManager(
payload: Awaited<ReturnType<typeof getPayload>>,
user: User,
): Promise<boolean> {
return await hasPermission(payload, user, "market-listings:update");
}
@ -344,7 +347,7 @@ async function resolveNpcOffer(
userId: buyer.id as number,
type: "market:counter",
title: "The vendor countered",
message: `${assetNameOf(listing)} — vendor proposes ${response.amount.toLocaleString()} each.`,
message: `${assetNameOf(listing)}: vendor proposes ${response.amount.toLocaleString()} each.`,
link: "/logistics/market",
});
await emitGameEvent(payload, {
@ -1033,8 +1036,7 @@ export async function getReferencePrices(
for (const asset of assets.docs) {
const base = getBaseBuyPrice(asset);
const modifier = modifierByItemId.get(asset.id);
references[asset.id] =
base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
references[asset.id] = base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
}
return { success: true, data: references };
} catch (error) {

View file

@ -9,7 +9,7 @@ import { ensureLockerStorage } from "@/lib/locker";
import { getCurrencyConfig } from "@/lib/banking";
export const metadata = {
title: "Market — Polaris Task Force",
title: "Market: Polaris Task Force",
};
export default async function MarketPage() {

View file

@ -5,7 +5,7 @@ import { PackageOpenIcon } from "lucide-react";
import { ResourceCard } from "@/components/frontend/logistics/ResourceCard";
export const metadata = {
title: "Resources — Polaris Task Force",
title: "Resources: Polaris Task Force",
};
export default async function ResourcesPage() {

View file

@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
import { ShipmentsList } from "@/components/frontend/logistics/ShipmentsList";
export const metadata = {
title: "Shipments — Polaris Task Force",
title: "Shipments: Polaris Task Force",
};
export default async function ShipmentsPage() {

View file

@ -683,7 +683,7 @@ export async function moveGridItem(
for (let dx = 0; dx < ow; dx++) {
for (let dy = 0; dy < oh; dy++) {
if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) {
return { success: false, error: "Cannot move here — overlaps with another item." };
return { success: false, error: "Cannot move here: overlaps with another item." };
}
}
}
@ -1134,7 +1134,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
message:
recovered > 0
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
: `No void items could be recovered in "${structure.name}" — grid full`,
: `No void items could be recovered in "${structure.name}": grid full`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",
@ -1146,7 +1146,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
error:
recovered > 0
? `${recovered} item(s) retrieved from void storage.`
: "No items could be retrieved — grid is full.",
: "No items could be retrieved: the grid is full.",
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
@ -1297,11 +1297,11 @@ export async function autoArrangeGrid(structureId: number): Promise<GridActionRe
const messages: string[] = [];
if (recoveredCount > 0) messages.push(`${recoveredCount} item(s) recovered from void.`);
if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void — grid full.`);
if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void (grid full).`);
await emitGameEvent(payload, {
type: EventTypes.GridArrange,
message: `Arranged grid in "${structure.name}"${messages.length > 0 ? ` — ${messages.join("; ")}` : ""}`,
message: `Arranged grid in "${structure.name}"${messages.length > 0 ? `: ${messages.join("; ")}` : ""}`,
actor: user.id,
structure: structureId,
targetCollection: "game-structures",

View file

@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
import { VehiclesList } from "./VehiclesList";
export const metadata = {
title: "Vehicles — Polaris Task Force",
title: "Vehicles: Polaris Task Force",
};
export default async function VehiclesPage() {

View file

@ -5,7 +5,7 @@ import { BellIcon } from "lucide-react";
import { NotificationsInbox } from "@/components/frontend/notifications/NotificationsInbox";
export const metadata = {
title: "Notifications — Polaris Task Force",
title: "Notifications: Polaris Task Force",
};
export default async function NotificationsPage() {

View file

@ -5,7 +5,7 @@ import { UsersIcon } from "lucide-react";
import { NpcDirectory } from "@/components/frontend/personnel/NpcDirectory";
export const metadata = {
title: "Contacts — Polaris Task Force",
title: "Contacts: Polaris Task Force",
};
export default async function ContactsPage() {
@ -25,8 +25,7 @@ export default async function ContactsPage() {
<h1 className="text-lg font-semibold">Contacts</h1>
</div>
<p className="text-sm text-muted-foreground">
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of
operations.
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of operations.
</p>
</div>

View file

@ -2,16 +2,10 @@ import config from "@payload-config";
import { getPayload } from "payload";
import type { GameNpc, MarketListing } from "@/payload-types";
import { TrendingUp } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export const metadata = {
title: "Statistics — Polaris Task Force",
title: "Statistics (Polaris Task Force)",
};
interface FactionStatistic {
@ -49,10 +43,7 @@ export default async function StatisticsPage() {
payload.find({
collection: "market-listings",
where: {
and: [
{ isAutoGenerated: { equals: true } },
{ status: { equals: "active" } },
],
and: [{ isAutoGenerated: { equals: true } }, { status: { equals: "active" } }],
},
limit: 200,
depth: 0,
@ -196,10 +187,7 @@ export default async function StatisticsPage() {
) : (
<div className="flex flex-col gap-4">
{topVendors.map((vendor) => (
<div
key={vendor.name}
className="flex items-center justify-between gap-4"
>
<div key={vendor.name} className="flex items-center justify-between gap-4">
<div className="min-w-0">
<p className="truncate text-sm font-semibold">{vendor.name}</p>
<p className="truncate text-xs text-muted-foreground">

View file

@ -654,8 +654,8 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
<span>{xpLevel.currentLevelXP.toLocaleString()} XP</span>
{xpLevel.nextLevelXP !== null ? (
<span>
{Math.floor(xpLevel.progress)}% — {xpLevel.nextLevelXP.toLocaleString()} XP to
next level
{Math.floor(xpLevel.progress)}% ({xpLevel.nextLevelXP.toLocaleString()} XP to next
level)
</span>
) : (
<span>Max level reached</span>

View file

@ -6,7 +6,7 @@ import { ProjectBoardView } from "@/components/frontend/projects/ProjectBoardVie
import { hasPermission } from "@/utils/access-control/hasPermission";
export const metadata = {
title: "Board — Polaris Task Force",
title: "Board | Polaris Task Force",
};
const KANBAN_STATUSES = [

View file

@ -7,14 +7,10 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
export const metadata = {
title: "Releases — Polaris Task Force",
title: "Releases: Polaris Task Force",
};
export default async function ReleasesPage({
params,
}: {
params: Promise<{ key: string }>;
}) {
export default async function ReleasesPage({ params }: { params: Promise<{ key: string }> }) {
const { key } = await params;
const headers = await nextHeaders();
const payload = await getPayload({ config });
@ -62,9 +58,10 @@ export default async function ReleasesPage({
const releases = releaseResult.docs.map((r: any) => {
const releaseTickets = tickets.filter((ticket: any) => {
const releaseId = typeof ticket.release === "object" && ticket.release !== null
? ticket.release.id
: ticket.release;
const releaseId =
typeof ticket.release === "object" && ticket.release !== null
? ticket.release.id
: ticket.release;
return releaseId === r.id;
});

View file

@ -6,7 +6,7 @@ import { SprintsView } from "@/components/frontend/projects/SprintsView";
import { hasPermission } from "@/utils/access-control/hasPermission";
export const metadata = {
title: "Sprints — Polaris Task Force",
title: "Sprints: Polaris Task Force",
};
export default async function SprintsPage({

View file

@ -7,7 +7,7 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
export const metadata = {
title: "Projects — Polaris Task Force",
title: "Projects: Polaris Task Force",
};
export default async function ProjectsPage() {

View file

@ -5,7 +5,7 @@ import { getPayload } from "payload";
import { ProjectSearchView } from "@/components/frontend/projects/ProjectSearchView";
export const metadata = {
title: "Search — Polaris Task Force",
title: "Search: Polaris Task Force",
};
export default async function SearchPage() {

View file

@ -9,7 +9,7 @@ import type { AimTrainerStats } from "@/app/(frontend)/qualification/actions";
import { AimTrainerArena } from "@/components/frontend/qualification/AimTrainerArena";
export const metadata: Metadata = {
title: "Qualification Course — Polaris Task Force",
title: "Qualification Course: Polaris Task Force",
description: "Complete a timed qualification course to sharpen your marksmanship and earn XP.",
};

View file

@ -7,7 +7,7 @@ import { Radio } from "lucide-react";
import { RadioArena } from "@/components/frontend/radio/RadioArena";
export const metadata: Metadata = {
title: "Radio Traffic — Polaris Task Force",
title: "Radio Traffic (Polaris Task Force)",
description: "Intercept and type incoming radio traffic fast + accurately.",
};

View file

@ -7,7 +7,7 @@ import { UsersIcon } from "lucide-react";
import { RosterView } from "@/components/frontend/roster/RosterView";
export const metadata = {
title: "Unit Roster — Polaris Task Force",
title: "Unit Roster (Polaris Task Force)",
};
export type RosterMember = {

View file

@ -8,7 +8,7 @@ import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
import { isSuperuser } from "@/utils/access-control/hasPermission";
export const metadata = {
title: "Transfers — Polaris Task Force",
title: "Transfers: Polaris Task Force",
};
export default async function TransfersPage() {

View file

@ -12,7 +12,7 @@ import { getScriptForUser } from "@/lib/voiceover";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
export const metadata: Metadata = {
title: "Record voiceover — Polaris Task Force",
title: "Record voiceover: Polaris Task Force",
description: "Read a Polaris Task Force voiceover script and submit a recording.",
};

View file

@ -21,7 +21,7 @@ import { cn } from "@/lib/utils";
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
export const metadata: Metadata = {
title: "Voiceover — Polaris Task Force",
title: "Voiceover: Polaris Task Force",
description: "Record voiceover takes for active Polaris Task Force scripts.",
};

View file

@ -10,7 +10,7 @@ import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
import { listTemplates, templateMap } from "@/lib/wiki/service";
export const metadata = {
title: "Edit wiki page — Polaris Task Force",
title: "Edit wiki page: Polaris Task Force",
};
function templateCssMap(

View file

@ -11,7 +11,7 @@ import { RevisionList, type WikiRevisionSummary } from "@/components/frontend/wi
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
export const metadata = {
title: "Wiki history — Polaris Task Force",
title: "Wiki history: Polaris Task Force",
};
function editorName(editor: WikiRevision["editor"]): string {

View file

@ -16,7 +16,7 @@ import { prepareMarkdown } from "@/lib/wiki/prepare";
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
export const metadata = {
title: "Wiki page — Polaris Task Force",
title: "Wiki page: Polaris Task Force",
};
function relationName(relation: WikiPageDocument["lastEditor"]): string {

View file

@ -251,7 +251,7 @@ export async function uploadWikiImage(
overrideAccess: true,
});
if (!doc.filename) {
return { success: false, error: "Upload failed — no file was stored." };
return { success: false, error: "Upload failed: no file was stored." };
}
return {
success: true,

View file

@ -7,7 +7,7 @@ import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
import { listTemplates, templateMap } from "@/lib/wiki/service";
export const metadata = {
title: "New wiki page — Polaris Task Force",
title: "New wiki page (Polaris Task Force)",
};
function templateCssMap(

View file

@ -7,7 +7,7 @@ import { WikiIndex, type WikiIndexPage } from "@/components/frontend/wiki/WikiIn
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
export const metadata = {
title: "Wiki — Polaris Task Force",
title: "Wiki: Polaris Task Force",
};
function relationName(relation: WikiPageDocument["lastEditor"]): string {

View file

@ -40,7 +40,7 @@ const resolveTargetMissions = async (
return { error: `No mission matches "${trimmed}" (searched by ID, name, and code name).` };
}
if (docs.length > 1) {
const list = docs.map((m) => `• ${missionLabel(m)} — #${m.id}`).join("\n");
const list = docs.map((m) => `• ${missionLabel(m)} (#${m.id})`).join("\n");
return { error: `Multiple missions match "${trimmed}". Be more specific:\n${list}` };
}
return { mission: docs[0] };
@ -92,20 +92,20 @@ export const remindEvaluationsCommand: BotCommand = {
)
) {
await interaction.editReply(
`**${missionLabel(mission)}** isn't evaluable yet — missions must be completed or past their scheduled start before reminders go out.`,
`**${missionLabel(mission)}** isn't evaluable yet: missions must be completed or past their scheduled start before reminders go out.`,
);
return;
}
const sent = await processMission(client, payload, mission);
await interaction.editReply(
`Done — ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
`Done: ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
);
return;
}
const { processed, sent } = await reconcile(client, payload);
await interaction.editReply(
`Sweep complete — ${processed} pending mission(s) processed, ${sent} reminder DM(s) sent.`,
`Sweep complete: ${processed} pending mission(s) processed, ${sent} reminder DM(s) sent.`,
);
} catch (error) {
await interaction.editReply(

View file

@ -19,11 +19,11 @@ const main = async () => {
let shuttingDown = false;
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
if (shuttingDown) {
payload.logger.warn(`[Bot] ${signal} received again — forcing exit`);
payload.logger.warn(`[Bot] ${signal} received again, forcing exit`);
process.exit(143);
}
shuttingDown = true;
payload.logger.info(`[Bot] ${signal} received — shutting down gracefully`);
payload.logger.info(`[Bot] ${signal} received, shutting down gracefully`);
try {
if (client.isReady()) {
await client.destroy();

View file

@ -61,11 +61,9 @@ const getReminderMissions = async (payload: Payload): Promise<Mission[]> => {
};
const formatLinks = (links: ReminderLink[], appUrl: string): string => {
const lines = links.map(
(link) => `• [${link.displayName}](${appUrl}${link.profilePath})`,
);
const lines = links.map((link) => `• [${link.displayName}](${appUrl}${link.profilePath})`);
// Discord embed field values cap at 1024 chars.
return (lines.join("\n").slice(0, 1024) || "—");
return lines.join("\n").slice(0, 1024) || "—";
};
const renderReminderEmbed = (
@ -75,7 +73,7 @@ const renderReminderEmbed = (
appUrl: string,
): EmbedBuilder => {
const embed = new EmbedBuilder()
.setTitle(`Leadership feedback requested — ${missionTitle(mission)}`)
.setTitle(`Leadership feedback requested: ${missionTitle(mission)}`)
.setColor(EMBED_COLOR)
.setFooter({
text: "Ratings are private and anonymous in aggregate. A leader's results become visible once 3+ members have rated them.",
@ -122,7 +120,12 @@ const sendReminderDms = async (
const user = await client.users.fetch(target.discordId as string);
await user.send({
embeds: [
renderReminderEmbed(mission, target.rateLeaders, target.rateSubordinates, botConfig.appUrl),
renderReminderEmbed(
mission,
target.rateLeaders,
target.rateSubordinates,
botConfig.appUrl,
),
],
});
sent += 1;
@ -152,7 +155,7 @@ export const processMission = async (
const { sent, capped } = await sendReminderDms(client, payload, mission);
if (capped) {
payload.logger.warn(
`[Bot] Evaluation reminders for mission #${mission.id}: DM cap hit — marker not set, will retry next tick.`,
`[Bot] Evaluation reminders for mission #${mission.id}: DM cap hit; marker not set, will retry next tick.`,
);
return sent;
}
@ -173,10 +176,7 @@ export interface ReconcileSummary {
sent: number;
}
export const reconcile = async (
client: Client,
payload: Payload,
): Promise<ReconcileSummary> => {
export const reconcile = async (client: Client, payload: Payload): Promise<ReconcileSummary> => {
if (reconcileRunning) return { processed: 0, sent: 0 };
reconcileRunning = true;
let processed = 0;

View file

@ -246,10 +246,7 @@ interface ModlistInfo {
updatedAt: string;
}
const resolveModlist = async (
payload: Payload,
mission: Mission,
): Promise<ModlistInfo | null> => {
const resolveModlist = async (payload: Payload, mission: Mission): Promise<ModlistInfo | null> => {
const raw = mission.gameDetails.gameSetup?.requiredMods;
if (raw == null || typeof raw === "number") return null;
const modlist = raw as ModList;
@ -282,7 +279,7 @@ const renderModlistEmbed = (
kind: "new" | "updated",
): EmbedBuilder => {
const embed = new EmbedBuilder()
.setTitle(kind === "new" ? `Modlist — ${modlist.name}` : `Modlist updated — ${modlist.name}`)
.setTitle(kind === "new" ? `Modlist: ${modlist.name}` : `Modlist updated: ${modlist.name}`)
.setColor(EMBED_COLOR)
.setTimestamp();
embed.setDescription(
@ -505,7 +502,8 @@ export const reconcile = async (client: Client, payload: Payload): Promise<void>
}
const eligible = (await getEligibleMissions(payload)).filter(
(mission) => isRollCallDue(mission) || !!mission.discordMessageId || !!mission.discordAttendanceSentAt,
(mission) =>
isRollCallDue(mission) || !!mission.discordMessageId || !!mission.discordAttendanceSentAt,
);
for (const mission of eligible) {
try {

View file

@ -82,7 +82,7 @@ export const linkDiscordAccount = async (
): Promise<{ user: User; wasLinked: boolean }> => {
const user = await findUserByDiscordUsername(payload, discordUsername);
if (!user) {
throw new SignupError("No account with that Discord username — use /signup to create one.");
throw new SignupError("No account with that Discord username. Use /signup to create one.");
}
if (user.discordId && user.discordId !== discordId) {
throw new SignupError(

View file

@ -219,7 +219,7 @@ export const GameRules: GlobalConfig = {
defaultValue: 60,
admin: {
description:
"Suggested economy tick cadence in minutes, for cron documentation only — the tick itself is triggered externally.",
"Suggested economy tick cadence in minutes, for cron documentation only. The tick itself is triggered externally.",
},
},
],

View file

@ -154,23 +154,23 @@ export const Missions: CollectionConfig = {
value: "external",
},
],
filterOptions: ({ req }) => {
return [
{
label: "Side Operation",
value: "side",
},
{
label: "External Operation",
value: "external",
},
{
label: "Main Operation",
value: "main",
},
];
},
defaultValue: ({ req }) => "side",
filterOptions: ({ req }) => {
return [
{
label: "Side Operation",
value: "side",
},
{
label: "External Operation",
value: "external",
},
{
label: "Main Operation",
value: "main",
},
];
},
defaultValue: ({ req }) => "side",
required: true,
},
{
@ -193,7 +193,8 @@ export const Missions: CollectionConfig = {
name: "discordAttendanceSentAt",
type: "date",
admin: {
description: "When the attendance roll-call was first posted to Discord (managed by the bot).",
description:
"When the attendance roll-call was first posted to Discord (managed by the bot).",
hidden: true,
},
},
@ -355,20 +356,20 @@ export const Missions: CollectionConfig = {
value: "private",
},
],
defaultValue: "leadership",
required: true,
},
{
name: "completedAt",
type: "date",
admin: {
description:
"When the mission transitioned to Completed. Set automatically by the lifecycle (auto-complete tick or manual status change) — managed here, not editable in the form.",
disabled: true,
},
},
],
},
defaultValue: "leadership",
required: true,
},
{
name: "completedAt",
type: "date",
admin: {
description:
"When the mission transitioned to Completed. Set automatically by the lifecycle (auto-complete tick or manual status change); managed here, not editable in the form.",
disabled: true,
},
},
],
},
{
name: "missionRoles",
type: "group",
@ -582,11 +583,15 @@ export const Missions: CollectionConfig = {
type: "text",
hidden: true,
defaultValue: "ptf313",
access: {
read: async ({ req }) => {
return await hasPermission(req.payload, req.user, "missions:read-sensitive");
},
},
access: {
read: async ({ req }) => {
return await hasPermission(
req.payload,
req.user,
"missions:read-sensitive",
);
},
},
},
],
},

View file

@ -633,7 +633,7 @@ export const Structures: CollectionConfig = {
defaultValue: false,
admin: {
description:
"When enabled, ONLY the items listed in Allowed Storage can be stored here — everything else is rejected. When disabled, the Allowed Storage list still enforces per-item quantity caps, but other items are permitted up to the mass capacity.",
"When enabled, ONLY the items listed in Allowed Storage can be stored here. Everything else is rejected. When disabled, the Allowed Storage list still enforces per-item quantity caps, but other items are permitted up to the mass capacity.",
},
},
{

View file

@ -36,7 +36,7 @@ export const Evaluations: CollectionConfig = {
required: true,
admin: {
description:
"Who submitted this evaluation. Private — only visible to users with evaluations:read.",
"Who submitted this evaluation. Private, only visible to users with evaluations:read.",
},
},
{
@ -80,8 +80,7 @@ export const Evaluations: CollectionConfig = {
name: "comment",
type: "textarea",
admin: {
description:
"Private free-text note. Never shown to normal users; managers only.",
description: "Private free-text note. Never shown to normal users; managers only.",
},
},
],

View file

@ -142,7 +142,7 @@ export const Roles: CollectionConfig = {
},
admin: {
description:
"Superuser roles bypass ALL permission checks. Use with extreme caution — this grants unrestricted access.",
"Superuser roles bypass ALL permission checks. Use with extreme caution: this grants unrestricted access.",
position: "sidebar",
},
},

View file

@ -227,7 +227,7 @@ export const Users: CollectionConfig = {
},*/
admin: {
description:
"Legacy role enum — being replaced by the dynamic RBAC system (see 'Role Assignments' below).",
"Legacy role enum, being replaced by the dynamic RBAC system (see 'Role Assignments' below).",
},
},
{

View file

@ -44,7 +44,7 @@ export const WikiTemplates: CollectionConfig = {
admin: {
language: "css",
description:
"Optional CSS injected on wiki pages while this template exists. Write plain selectors — they are automatically prefixed with a hash-based scope class (e.g. .ptf-tpl-3afb1c .my-class) so templates never collide with each other or the app shell.",
"Optional CSS injected on wiki pages while this template exists. Write plain selectors, which are automatically prefixed with a hash-based scope class (e.g. .ptf-tpl-3afb1c .my-class) so templates never collide with each other or the app shell.",
},
},
{
@ -57,7 +57,7 @@ export const WikiTemplates: CollectionConfig = {
},
admin: {
description:
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded — replaces the markdown body.",
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded, replacing the markdown body.",
},
},
],

View file

@ -32,7 +32,7 @@ export function DiscordUsernameRequestForm({
return;
}
setValue("");
setSuccess("Request submitted — leadership has been notified.");
setSuccess("Request submitted. Leadership has been notified.");
router.refresh();
} catch {
setError("Something went wrong. Please try again.");

View file

@ -314,7 +314,7 @@ export function StaffRoster({
{laborCrews.length === 0 ? (
<p className="text-sm text-muted-foreground">
No labor crew — add workers via Hire Staff.
No labor crew. Add workers via Hire Staff.
</p>
) : (
<div className="flex flex-col">
@ -329,7 +329,7 @@ export function StaffRoster({
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<p className="font-medium">
{crew.classificationName} — {crew.headcount} hired ·{" "}
{crew.classificationName}: {crew.headcount} hired ·{" "}
{crew.requiredHeadcount} required · max {crew.maxHeadcount}
</p>
<p className="font-mono text-sm tabular-nums text-muted-foreground">

View file

@ -190,7 +190,7 @@ export function EodLeaderboard({
</div>
) : rows.length === 0 ? (
<p className="px-4 py-4 text-sm text-muted-foreground">
No clearances logged yet — take the first sector!
No clearances logged yet. Take the first sector!
</p>
) : (
<div className="overflow-x-auto">
@ -218,7 +218,7 @@ export function EodLeaderboard({
)}
title={
row.isOutsideTopTen
? "Operator status — not currently ranked in the top 10"
? "Operator status (not currently ranked in the top 10)"
: undefined
}
>
@ -238,13 +238,14 @@ export function EodLeaderboard({
unranked
</span>
)}
{row.userId !== null && activeTeams.some((team) => team.id === row.userId) && (
<span
className="size-2 rounded-full bg-emerald-400"
title="Clearing now"
aria-label="Clearing now"
/>
)}
{row.userId !== null &&
activeTeams.some((team) => team.id === row.userId) && (
<span
className="size-2 rounded-full bg-emerald-400"
title="Clearing now"
aria-label="Clearing now"
/>
)}
</span>
</td>
<td className="px-2 py-2 text-right tabular-nums text-muted-foreground">

View file

@ -807,8 +807,8 @@ export function MinesweeperBoard({
aria-live="polite"
>
<div className="flex flex-col gap-0.5">
{phase === "ready" && <span>Awaiting deployment — select a sector.</span>}
{phase === "playing" && <span>Live sweep — mark every charge before you reveal.</span>}
{phase === "ready" && <span>Awaiting deployment. Select a sector.</span>}
{phase === "playing" && <span>Live sweep: mark every charge before you reveal.</span>}
{phase === "over" && lastRun?.won && (
<span>
Sector clear. {config.label} defused in {lastRun.seconds}s.

View file

@ -134,7 +134,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
</div>
) : rows.length === 0 ? (
<p className="px-4 py-4 text-sm text-muted-foreground">
No pilots on the board yet — be the first to pass a pipe!
No pilots on the board yet. Be the first to pass a pipe!
</p>
) : (
<table className="w-full text-sm">
@ -156,7 +156,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
row.isCurrentUser && "bg-accent/40",
row.isOutsideTopTen && "text-muted-foreground opacity-70",
)}
title={row.isOutsideTopTen ? "Pilot status — not currently ranked in the top 10" : undefined}
title={row.isOutsideTopTen ? "Pilot status: not currently ranked in the top 10" : undefined}
>
<td className="px-4 py-2">
<span className={rankClass(i)}>{row.isOutsideTopTen ? "—" : i + 1}</span>

View file

@ -246,7 +246,7 @@ export function CreateSideOpDialog({
return;
}
if (!isEdit && !defaultStartDateTime) {
setError("Missing start date — schedule from a Friday on the calendar.");
setError("Missing start date: schedule from a Friday on the calendar.");
return;
}
const startDateIso = defaultStartDateTime ?? "";

View file

@ -125,7 +125,7 @@ export function FridayCalendar({
<ItemContent>
{entry.missions.length === 0 ? (
<p className="text-sm text-muted-foreground">
Nothing scheduled yet — claim this Friday.
Nothing scheduled yet. Claim this Friday.
</p>
) : (
<div className="flex flex-col gap-1.5">

View file

@ -89,7 +89,7 @@ export function MissionAttendance({
{
id: `mission:${missionId}:rsvp-yes`,
group: "Mission RSVP",
label: "RSVP — Yes",
label: "RSVP: Yes",
icon: CheckIcon,
keywords: ["attend", "going", "yes", "rsvp"],
onSelect: () => void handleSet("yes"),
@ -97,7 +97,7 @@ export function MissionAttendance({
{
id: `mission:${missionId}:rsvp-tentative`,
group: "Mission RSVP",
label: "RSVP — Tentative",
label: "RSVP: Tentative",
icon: MinusIcon,
keywords: ["maybe", "tentative", "unsure", "rsvp"],
onSelect: () => void handleSet("tentative"),
@ -105,7 +105,7 @@ export function MissionAttendance({
{
id: `mission:${missionId}:rsvp-no`,
group: "Mission RSVP",
label: "RSVP — No",
label: "RSVP: No",
icon: XIcon,
keywords: ["decline", "not going", "no", "rsvp"],
onSelect: () => void handleSet("no"),

View file

@ -65,7 +65,7 @@ export function SideOpNarrativeFields({ value, onChange }: SideOpNarrativeFields
return (
<div className="grid gap-5">
<p className="text-xs text-muted-foreground">
Plain text only — each line becomes a paragraph. Rich formatting written in the admin panel
Plain text only. Each line becomes a paragraph. Rich formatting written in the admin panel
is flattened if you save here.
</p>

View file

@ -149,7 +149,7 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
{!equippedLoadout && (
<p className="text-sm text-muted-foreground">
Choose a loadout above — it becomes the active loadout that the wardrobe
Choose a loadout above. It becomes the active loadout that the wardrobe
reflects.
</p>
)}

View file

@ -156,7 +156,7 @@ export function ResourceCard({ resource }: ResourceCardProps) {
</div>
</ItemContent>
<ItemDescription>
{truncatedDescription ?? `${resource.type} resource — ${resource.unitOfMeasure}`}
{truncatedDescription ?? `${resource.type} resource (${resource.unitOfMeasure})`}
</ItemDescription>
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
View details <ChevronRightIcon className="size-3" />

View file

@ -34,7 +34,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
{
id: "shipments:filter-all",
group: "Shipments",
label: "Filter — All Shipments",
label: "Filter: All Shipments",
icon: ListFilterIcon,
keywords: ["filter", "all", "shipments"],
onSelect: () => setFilter(null),
@ -42,7 +42,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
{
id: "shipments:filter-active",
group: "Shipments",
label: "Filter — Active Shipments",
label: "Filter: Active Shipments",
icon: ClockIcon,
keywords: ["filter", "active", "in transit", "pending"],
onSelect: () => setFilter("active"),
@ -50,7 +50,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
{
id: "shipments:filter-completed",
group: "Shipments",
label: "Filter — Completed Shipments",
label: "Filter: Completed Shipments",
icon: CheckCircleIcon,
keywords: ["filter", "completed", "arrived", "done"],
onSelect: () => setFilter("completed"),
@ -58,7 +58,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
{
id: "shipments:filter-cancelled",
group: "Shipments",
label: "Filter — Cancelled Shipments",
label: "Filter: Cancelled Shipments",
icon: XCircleIcon,
keywords: ["filter", "cancelled", "failed"],
onSelect: () => setFilter("cancelled"),
@ -66,7 +66,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
{
id: "shipments:filter-stranded",
group: "Shipments",
label: "Filter — Stranded Shipments",
label: "Filter: Stranded Shipments",
icon: TruckIcon,
keywords: ["filter", "stranded", "stuck"],
onSelect: () => setFilter("stranded"),

View file

@ -213,7 +213,7 @@ export function CreateListingDialog({
value={String(s.assetId)}
disabled={!s.tradeable || !s.isLive}
>
{s.name} — {s.quantity.toLocaleString()}×
{s.name} ({s.quantity.toLocaleString()}×)
</SelectItem>
))}
</SelectContent>

View file

@ -155,7 +155,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
const boughtQty = thread?.quantity ?? negotiation?.quantity ?? stock;
setAcceptedAt(Date.now());
toast.success("Deal closed", {
description: `Sold ${boughtQty}× at ${fmt(agreed)} each — total ${fmt(agreed * boughtQty)}. The items are in your locker.`,
description: `Sold ${boughtQty}× at ${fmt(agreed)} each, for a total of ${fmt(agreed * boughtQty)}. The items are in your locker.`,
});
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
setBusy(null);
@ -168,7 +168,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
setAcceptedAt(Date.now());
const dealQty = data.quantity ?? stock;
toast.success("Deal closed", {
description: `The vendor accepted your offer of ${fmt(data.amount ?? 0)} each for ${dealQty}× — total ${fmt((data.amount ?? 0) * dealQty)}.`,
description: `The vendor accepted your offer of ${fmt(data.amount ?? 0)} each for ${dealQty}×, for a total of ${fmt((data.amount ?? 0) * dealQty)}.`,
});
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
setBusy(null);
@ -236,7 +236,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Negotiate — {assetName}</DialogTitle>
<DialogTitle>Negotiate: {assetName}</DialogTitle>
<DialogDescription>
{isVendor ? "Haggle with the vendor." : "Send an offer to the seller."} Asking{" "}
<span className="font-mono">{fmt(listing.price ?? 0)}</span> per unit.
@ -304,7 +304,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
<p className="text-sm">
The {isVendor ? "vendor" : "seller"} counteroffers{" "}
<span className="font-mono font-semibold">{fmt(negotiation?.amount ?? 0)}</span>{" "}
each for {negotiation?.quantity ?? stock}× — total{" "}
each for {negotiation?.quantity ?? stock}×, for a total of{" "}
<span className="font-mono">
{fmt((negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock))}
</span>
@ -331,7 +331,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
</Badge>
<p className="text-sm text-muted-foreground">
Sold at <span className="font-mono">{fmt(outcome.price)}</span> each for{" "}
{negotiation?.quantity ?? stock}× — total{" "}
{negotiation?.quantity ?? stock}×, for a total of{" "}
<span className="font-mono">
{fmt(outcome.price * (negotiation?.quantity ?? stock))}
</span>

View file

@ -97,7 +97,7 @@ function NegotiationRow({
if (key === "accept") {
const qty = negotiation.quantity ?? listing?.quantity ?? 1;
toast.success("Deal closed", {
description: `Sold ${qty}× at ${fmt(negotiation.amount ?? 0)} each — total ${fmt((negotiation.amount ?? 0) * qty)}.`,
description: `Sold ${qty}× at ${fmt(negotiation.amount ?? 0)} each, total ${fmt((negotiation.amount ?? 0) * qty)}.`,
});
}
router.refresh();

View file

@ -21,7 +21,7 @@ export function NpcChat({ listing, negotiation, currencyLabel }: NpcChatProps) {
<div className="flex items-center gap-2 border-b px-3 py-2">
<StoreIcon className="size-4 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground">Vendor</span>
<span className="text-xs text-muted-foreground">— {assetName}</span>
<span className="text-xs text-muted-foreground">{assetName}</span>
</div>
<ChatBubbleList bubbles={bubbles} buyerLabel="You" sellerLabel="Vendor" />
</div>

View file

@ -148,16 +148,16 @@ export function AimTrainerArena({
<div className="flex flex-col gap-1">
<span className="font-medium text-foreground">Acquire</span>
<span>
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile
silhouettes are active at once, plus an occasional blue civilian that never counts
toward the limit. Targets rebound off each other and the range walls.
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile silhouettes
are active at once, plus an occasional blue civilian that never counts toward the
limit. Targets rebound off each other and the range walls.
</span>
</div>
<div className="flex flex-col gap-1">
<span className="font-medium text-foreground">Prioritize</span>
<span>
Headshots score 250 points. Torso hits score 100 points. Blue civilians cost 100
points — never shoot them.
points, so never shoot them.
</span>
</div>
<div className="flex flex-col gap-1">

View file

@ -436,14 +436,14 @@ export function AimTrainerGame({
setLive(getAimTrainerLiveStats(game));
setPhaseSafe("over");
playRoundComplete();
void handleRoundCompleteRef.current({
difficulty,
headshots: game.headshots,
torsoHits: game.torsoHits,
civilianHits: game.civilianHits,
shotsFired: game.shotsFired,
despawnedUnhitTargets: game.despawnedUnhitTargets,
});
void handleRoundCompleteRef.current({
difficulty,
headshots: game.headshots,
torsoHits: game.torsoHits,
civilianHits: game.civilianHits,
shotsFired: game.shotsFired,
despawnedUnhitTargets: game.despawnedUnhitTargets,
});
};
const step = (now: number) => {
@ -624,7 +624,7 @@ export function AimTrainerGame({
Ready on the line
</span>
<span className="text-sm text-muted-foreground">
Acquire every silhouette before it clears the range — and never shoot the blue
Acquire every silhouette before it clears the range, and never shoot the blue
civilians.
</span>
</div>

View file

@ -143,7 +143,7 @@ export function AimTrainerLeaderboard({
</div>
) : rows.length === 0 ? (
<p className="px-4 py-4 text-sm text-muted-foreground">
No scores logged for this lane yet — take the first qualification slot.
No scores logged for this lane yet. Take the first qualification slot.
</p>
) : (
<div className="overflow-x-auto">
@ -172,7 +172,7 @@ export function AimTrainerLeaderboard({
)}
title={
row.isOutsideTopTen
? "Operator status — not currently ranked in the top 10"
? "Operator status: not currently ranked in the top 10"
: undefined
}
>

View file

@ -509,7 +509,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
const vType = typeof v.type === "object" ? v.type : null;
return (
<SelectItem key={v.id} value={String(v.id)}>
{v.name} ({vType?.name ?? "Unknown"}) — {v.currentFuel?.toFixed(0)} fuel
{v.name} ({vType?.name ?? "Unknown"}), {v.currentFuel?.toFixed(0)} fuel
</SelectItem>
);
})}
@ -683,7 +683,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
{cap !== undefined && (
<span className="text-muted-foreground">
{" "}
— {cap.toLocaleString()} max
({cap.toLocaleString()} max)
</span>
)}
</SelectItem>
@ -868,7 +868,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
if (!isResourceWhitelisted(destType, resId)) {
return (
<span className="text-xs text-amber-400">
Destination only accepts items on its allowed storage list — this
Destination only accepts items on its allowed storage list. This
resource is not permitted.
</span>
);

View file

@ -32,7 +32,7 @@ export function StorageRules({
<div className="flex items-center gap-2 p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-xs text-amber-400">
<ListRestartIcon className="size-3.5 shrink-0" />
<span>
Whitelist mode — only the items listed under "Allowed" can be stored here. All other
Whitelist mode: only the items listed under "Allowed" can be stored here. All other
items are rejected.
</span>
</div>

View file

@ -99,7 +99,7 @@ export function StructureCard({ structure }: StructureCardProps) {
</ItemContent>
<ItemDescription>
{resourceCount === 0
? "Empty — no resources stored."
? "Empty: no resources stored."
: `${resourceCount} resource type${resourceCount !== 1 ? "s" : ""} stored`}
</ItemDescription>
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">

View file

@ -210,7 +210,7 @@ export function StructureStorageView({ structure }: StructureStorageViewProps) {
</ItemTitle>
<ItemDescription className="flex items-center gap-2">
<span>
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""} — these no
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""}: these no
longer fit in the grid.
</span>
<button

View file

@ -370,12 +370,12 @@ export function TeleprompterReader({ script }: { readonly script: string }) {
<div className="flex flex-col gap-1 rounded-md border border-border bg-background/50 p-3 text-xs text-muted-foreground">
<p>
<span className="font-medium text-foreground">How to read:</span> read every word shown
in large text — underlined and plain alike.
in large text, underlined and plain alike.
</p>
<p>
The small colored labels above the text (
<span className="italic text-sky-300/80">like this</span>) are directions for how to
speak — never read them aloud. Each underlined stretch is the wording its label above
speak, never read them aloud. Each underlined stretch is the wording its label above
applies to.
</p>
<p>

View file

@ -59,7 +59,7 @@ export function MarkdownGuide() {
<GuideSection title="Footnotes & citations">
<Example>Text.[^1]</Example>
<Example>[^1]: Footnote or citation — source, URL</Example>
<Example>[^1]: Footnote or citation (source, URL)</Example>
</GuideSection>
<GuideSection title="Layout">
@ -98,12 +98,12 @@ Right column content
</GuideSection>
<GuideSection title="Keybinds">
<Example>Ctrl/Cmd+B — bold</Example>
<Example>Ctrl/Cmd+I — italic</Example>
<Example>Ctrl/Cmd+Shift+X — strikethrough</Example>
<Example>Ctrl/Cmd+Shift+H — heading</Example>
<Example>Ctrl/Cmd+K — open wikilink dialog</Example>
<Example>Ctrl/Cmd+E — inline code</Example>
<Example>Ctrl/Cmd+B: bold</Example>
<Example>Ctrl/Cmd+I: italic</Example>
<Example>Ctrl/Cmd+Shift+X: strikethrough</Example>
<Example>Ctrl/Cmd+Shift+H: heading</Example>
<Example>Ctrl/Cmd+K: open wikilink dialog</Example>
<Example>Ctrl/Cmd+E: inline code</Example>
</GuideSection>
</div>
);

View file

@ -63,7 +63,7 @@ export async function markStaleServersOffline(
});
await emitGameEvent(payload, {
type: EventTypes.ServerOffline,
message: `Server "${server.name}" (${server.serverId}) marked offline — no heartbeat for ${Math.round(
message: `Server "${server.name}" (${server.serverId}) marked offline: no heartbeat for ${Math.round(
SERVER_OFFLINE_AFTER_MS / 1000,
)}s.`,
targetCollection: "game-servers",

View file

@ -56,7 +56,7 @@ export async function processPlayerKill(
const user = users.docs[0];
if (!user) {
payload.logger.warn(
`[KillTracker] No user found for Steam ID ${kill.killerSteamId} — kill not recorded.`,
`[KillTracker] No user found for Steam ID ${kill.killerSteamId}; kill not recorded.`,
);
return;
}
@ -71,7 +71,7 @@ export async function processPlayerKill(
const profile = profiles.docs[0];
if (!profile) {
payload.logger.warn(
`[KillTracker] No profile found for user ${user.id} (${user.username}) — kill not recorded.`,
`[KillTracker] No profile found for user ${user.id} (${user.username}); kill not recorded.`,
);
return;
}

View file

@ -238,7 +238,7 @@ export async function applyTransaction(
type,
amount: -totalDebit,
balanceAfter: fromNewBalance,
memo: memo ?? `${type} — out`,
memo: memo ?? `${type}: out`,
timestamp,
});
}
@ -249,7 +249,7 @@ export async function applyTransaction(
type,
amount,
balanceAfter: toNewBalance,
memo: memo ?? `${type} — in`,
memo: memo ?? `${type}: in`,
timestamp,
});
}

View file

@ -36,7 +36,7 @@ export async function hireStaff(
): Promise<NpcStaffing> {
await assertStaffHiringEnabled(payload);
if (input.positionType === "labor") {
throw new Error("Basic labor is tracked by headcount — use the labor count control instead.");
throw new Error("Basic labor is tracked by headcount. Use the labor count control instead.");
}
const positionType = staffingPositionType(input.positionType);
const npc = await payload

View file

@ -39,7 +39,7 @@ async function processSalaries(
: staffing.positionTitle;
const memo = isLabor
? `Labor: ${headcount} ${classificationLabel}(s) @ ${unitSalary}`
: `Salary: ${staffing.positionTitle} — ${npcName}`;
: `Salary: ${staffing.positionTitle} (${npcName})`;
try {
await applyTransaction(payload, {
type: "salary",

View file

@ -81,7 +81,7 @@ export async function autoCompleteMissions(
});
await emitGameEvent(payload, {
type: EventTypes.MissionAutoComplete,
message: `Mission "${mission.name}"${mission.codeName ? ` (${mission.codeName})` : ""} auto-completed — the scheduled day has passed.`,
message: `Mission "${mission.name}"${mission.codeName ? ` (${mission.codeName})` : ""} auto-completed: the scheduled day has passed.`,
targetCollection: "missions",
targetId: mission.id,
data: { previousStatus },

View file

@ -6,13 +6,13 @@ type DialogueLine = (amount: string, asking: string) => string;
const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
firm: [
(amount) => `I'm not going any lower than ${amount}. Take it or leave it.`,
(amount) => `That's my final price — ${amount}. I've got other buyers knocking.`,
(amount) => `That's my final price: ${amount}. I've got other buyers knocking.`,
(amount) => `I'm holding at ${amount}. My prices are already fair.`,
],
lowball: [
(amount) => `That's an insult! I'll come down to ${amount} — not a fraction less.`,
(amount) => `That's an insult! I'll come down to ${amount}, not a fraction less.`,
(amount) => `You're joking, right? ${amount} and not a fraction less.`,
(amount) => `I paid more than that for lunch! ${amount} — final offer.`,
(amount) => `I paid more than that for lunch! ${amount}, final offer.`,
],
backward: [
(amount) => `Now you're offering less than before? I'm holding firm at ${amount}.`,
@ -26,22 +26,22 @@ const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
concede: [
(amount) => `Hmm, not sure about that... how about ${amount}?`,
(amount) => `I could stretch to ${amount}... if we're serious about this.`,
(amount) => `Let's not waste each other's time — ${amount}.`,
(amount) => `Let's not waste each other's time. ${amount}.`,
(amount) => `Alright, I'll bend a little. ${amount}?`,
],
good: [
() => "Now we're talking. That's a fair price — you've got a deal.",
() => "Now we're talking. That's a fair price, and you've got a deal.",
() => "You drive a hard bargain, but that's a price I can live with.",
() => "That works for me. A man's gotta make a living, after all.",
],
overpay: [
() => "Wait — more than I asked? Sold! You won't regret it.",
() => "Wait, more than I asked? Sold! You won't regret it.",
() => "Generous! I'd have taken less, but I won't argue. It's yours.",
() => "Whoa, okay! Not going to talk you out of that one. Done deal.",
],
closed: [
(_amount, asking) => `Alright, that's enough haggling. Buy it at ${asking} or not at all.`,
(_amount, asking) => `I'm done negotiating. The price is ${asking} — take it or leave it.`,
(_amount, asking) => `I'm done negotiating. The price is ${asking}, take it or leave it.`,
],
accept: [
() => "I think that price is acceptable. Let's shake on it.",

View file

@ -102,12 +102,12 @@ export function storageViolationMessage(resourceName: string, check: StorageRule
case "prohibited":
return `This resource is prohibited from storage at this structure.`;
case "not_whitelisted":
return `This structure only accepts items on its allowed storage list — "${resourceName}" is not permitted.`;
return `This structure only accepts items on its allowed storage list: "${resourceName}" is not permitted.`;
case "over_cap": {
const available = Math.max(0, (check.cap ?? 0) - (check.currentStored ?? 0));
return available <= 0
? `${resourceName} is at its storage allowance here (${check.cap?.toLocaleString()}). No more can be stored.`
: `${resourceName} is limited to ${check.cap?.toLocaleString()} units here — you can add at most ${available.toLocaleString()} more.`;
: `${resourceName} is limited to ${check.cap?.toLocaleString()} units here. You can add at most ${available.toLocaleString()} more.`;
}
default:
return `This resource cannot be stored here.`;

View file

@ -31,7 +31,7 @@ export const script = async (config: SanitizedConfig) => {
});
if (result.disabled) {
log.info("[Economy Tick] Master gate is off — nothing processed.");
log.info("[Economy Tick] Master gate is off, nothing processed.");
} else {
await emitEconomyEvents(payload, result);
log.info(

View file

@ -55,7 +55,7 @@ export const script = async (config: SanitizedConfig) => {
await emitGameEvent(payload, {
type: EventTypes.MarketListingExpire,
message: `Listing for ${listing.quantity}× ${assetName} expired${
isUserListing ? " — stock returned to seller" : ""
isUserListing ? ", stock returned to seller" : ""
}`,
actor: sellerId ?? undefined,
targetCollection: "market-listings",

View file

@ -56,7 +56,7 @@ export async function processShipmentTick(
await emitGameEvent(payload, {
type: EventTypes.ShipmentFail,
message: `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" stranded — vehicle out of fuel`,
message: `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" stranded: vehicle out of fuel`,
structure: originId,
targetCollection: "shipments",
targetId: shipment.id,
@ -231,7 +231,7 @@ async function completeShipment(
status: "failed",
failureReason: `Destination storage rules prevented delivery: ${bounced
.map((b) => `${b.resourceName} ×${b.amount.toLocaleString()}`)
.join(", ")} — returned to origin.`,
.join(", ")}; returned to origin.`,
completedAt: new Date().toISOString(),
},
});
@ -321,7 +321,7 @@ async function createAutoReturnShipment(
if (vehicle.currentFuel < returnFuelCost) {
await emitGameEvent(payload, {
type: EventTypes.ShipmentFail,
message: `Auto-return failed — insufficient fuel`,
message: `Auto-return failed: insufficient fuel`,
structure: destinationId,
targetCollection: "game-vehicles",
targetId: vehicle.id,

View file

@ -174,9 +174,7 @@ function run(
stderr += s;
process.stderr.write(s);
});
child.on("close", (code) =>
resolve({ code: code ?? 1, stdout, stderr }),
);
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
});
}
@ -186,16 +184,14 @@ async function pickEnv(forced?: string): Promise<{ env: EnvName; uri: string }>
if (forced && forced in ENVS) {
const uri = loadDatabaseUri(forced as EnvName);
if (!uri) {
log(
`${RED}Cannot find DATABASE_URI in ${ENVS[forced as EnvName].file}${RESET}`,
);
log(`${RED}Cannot find DATABASE_URI in ${ENVS[forced as EnvName].file}${RESET}`);
log(`Create the file with: DATABASE_URI=postgres://user:pass@host:5432/db`);
process.exit(1);
}
return { env: forced as EnvName, uri };
}
log(`\n${BOLD}⚡ Polaris Task Force — Migration Runner${RESET}\n`);
log(`\n${BOLD}⚡ Polaris Task Force: Migration Runner${RESET}\n`);
log("Target environment:\n");
for (let i = 0; i < ENV_ORDER.length; i++) {
@ -220,9 +216,7 @@ async function pickEnv(forced?: string): Promise<{ env: EnvName; uri: string }>
const uri = loadDatabaseUri(env);
if (!uri) {
log(`\n${RED}DATABASE_URI not found in ${ENVS[env].file}${RESET}`);
log(
`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`,
);
log(`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`);
process.exit(1);
}
@ -259,9 +253,7 @@ async function main() {
if (freshOnly) {
if (env !== "dev") {
log(`${RED}${BOLD}--fresh is only allowed for dev.${RESET}`);
log(
`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`,
);
log(`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`);
process.exit(1);
}
const confirm = await ask(
@ -281,11 +273,9 @@ async function main() {
// Show current status
log(`${DIM}Migration status:${RESET}\n`);
const statusResult = await run(
"bun",
["run", "payload", "migrate:status"],
{ DATABASE_URI: uri },
);
const statusResult = await run("bun", ["run", "payload", "migrate:status"], {
DATABASE_URI: uri,
});
// Check for pending migrations
const hasPending = statusResult.stdout.includes("│ No │");
@ -297,26 +287,16 @@ async function main() {
// Confirm
log("");
if (env === "prd") {
log(
`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`,
);
log(
`${RED}${BOLD} ║ YOU ARE ABOUT TO MIGRATE PRODUCTION ║${RESET}`,
);
log(
`${RED}${BOLD} ╚══════════════════════════════════════════╝${RESET}\n`,
);
const confirm = await ask(
`${RED}Type "migrate" to confirm: ${RESET}`,
);
log(`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
log(`${RED}${BOLD} ║ YOU ARE ABOUT TO MIGRATE PRODUCTION ║${RESET}`);
log(`${RED}${BOLD} ╚══════════════════════════════════════════╝${RESET}\n`);
const confirm = await ask(`${RED}Type "migrate" to confirm: ${RESET}`);
if (confirm.trim() !== "migrate") {
log("Aborted.");
return;
}
} else {
const confirm = await ask(
`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`,
);
const confirm = await ask(`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`);
if (confirm.toLowerCase() !== "y") {
log("Aborted.");
return;
@ -327,12 +307,7 @@ async function main() {
// push:true's batch=-1 record) is auto-accepted instead of blocking on
// non-interactive stdin.
log(`\n${BOLD}Running migrations...${RESET}\n`);
const result = await run(
"bun",
["run", "payload", "migrate"],
{ DATABASE_URI: uri },
"y\n",
);
const result = await run("bun", ["run", "payload", "migrate"], { DATABASE_URI: uri }, "y\n");
if (result.code !== 0) {
log(`\n${RED}${BOLD}✗ Migration failed (exit ${result.code})${RESET}`);

View file

@ -149,7 +149,7 @@ function planImport(
async function resolveFuelConfig(payload: { find: (args: any) => Promise<{ docs: any[] }> }): Promise<{ id: number }> {
const r = await payload.find({ collection: "resources", limit: 50, where: { name: { equals: "Fuel" } } });
const fuel = r.docs.find((d: any) => String(d.name).toLowerCase() === "fuel");
if (!fuel) throw new Error("No resource named 'Fuel' found — cannot set vehicle fuel fields");
if (!fuel) throw new Error("No resource named 'Fuel' found. Cannot set vehicle fuel fields");
return { id: fuel.id };
}

View file

@ -37,7 +37,7 @@ const DEFAULT_XP_LEVELS = [
name: "Recruit",
requiredPoints: 0,
description: richText(
"Fresh out of boot camp. Everything is new — follow orders and learn the ropes.",
"Fresh out of boot camp. Everything is new; follow orders and learn the ropes.",
),
},
{