1
0
Fork 0

Compare commits

..

No commits in common. "a2a5f7877bef6fce69b43fd9f65db030cc897a40" and "95887e56cee2a4ab1c569a22b6103e2b544cfce0" have entirely different histories.

96 changed files with 293 additions and 267 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,9 +114,7 @@ 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} by ${reporterName}`,
message: `${title} — ${reporterName}`,
link: `/helpdesk/${createdTicket.id}`,
});
}
@ -233,7 +233,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
targetId: ticketId,
});
const replyMessage = `"${ticket.title}" has a new reply.`;
const replyMessage = `"${ticket.title}" — you have 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() {
@ -38,7 +38,7 @@ export default async function LockerPage() {
});
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,10 +47,7 @@ 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");
}
@ -347,7 +344,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, {
@ -1036,7 +1033,8 @@ 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: the grid is full.",
: "No items could be retrieved — 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,7 +25,8 @@ 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,10 +2,16 @@ 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 {
@ -43,7 +49,10 @@ 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,
@ -187,7 +196,10 @@ 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,10 +7,14 @@ 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 });
@ -58,8 +62,7 @@ export default async function ReleasesPage({ params }: { params: Promise<{ key:
const releases = releaseResult.docs.map((r: any) => {
const releaseTickets = tickets.filter((ticket: any) => {
const releaseId =
typeof ticket.release === "object" && ticket.release !== null
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,9 +61,11 @@ 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 = (
@ -73,7 +75,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.",
@ -120,12 +122,7 @@ 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;
@ -155,7 +152,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;
}
@ -176,7 +173,10 @@ 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,7 +246,10 @@ 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;
@ -279,7 +282,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(
@ -502,8 +505,7 @@ 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

@ -193,8 +193,7 @@ 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,
},
},
@ -364,7 +363,7 @@ export const Missions: CollectionConfig = {
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.",
"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,
},
},
@ -585,11 +584,7 @@ export const Missions: CollectionConfig = {
defaultValue: "ptf313",
access: {
read: async ({ req }) => {
return await hasPermission(
req.payload,
req.user,
"missions:read-sensitive",
);
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,7 +80,8 @@ 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

@ -71,11 +71,16 @@ export const VoiceSubmissions: CollectionConfig = {
},
},
],
// No `upload.mimeTypes` allowlist on purpose: Payload content-sniffs uploads
// (file-type) and reports WebM recordings as `video/webm`, so an audio/* MIME
// allowlist rejects browser-recorded takes. The submit action is the gate
// instead (audio/* base type + size cap); restricted file types (executables,
// scripts, …) stay blocked by Payload's built-in checks.
upload: true,
upload: {
mimeTypes: [
"audio/webm",
"audio/mpeg",
"audio/wav",
"audio/ogg",
"audio/mp4",
"audio/x-m4a",
"audio/m4a",
],
},
timestamps: true,
};

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, 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.",
"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.",
},
},
{
@ -57,7 +57,7 @@ export const WikiTemplates: CollectionConfig = {
},
admin: {
description:
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded, replacing the markdown body.",
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded — replaces 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,8 +238,7 @@ export function EodLeaderboard({
unranked
</span>
)}
{row.userId !== null &&
activeTeams.some((team) => team.id === row.userId) && (
{row.userId !== null && activeTeams.some((team) => team.id === row.userId) && (
<span
className="size-2 rounded-full bg-emerald-400"
title="Clearing now"

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, for a total of ${fmt(agreed * boughtQty)}. The items are in your locker.`,
description: `Sold ${boughtQty}× at ${fmt(agreed)} each — total ${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}×, for a total of ${fmt((data.amount ?? 0) * dealQty)}.`,
description: `The vendor accepted your offer of ${fmt(data.amount ?? 0)} each for ${dealQty}× — total ${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}×, for a total of{" "}
each for {negotiation?.quantity ?? stock}× — total{" "}
<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}×, for a total of{" "}
{negotiation?.quantity ?? stock}× — total{" "}
<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, so never shoot them.
points — never shoot them.
</span>
</div>
<div className="flex flex-col gap-1">

View file

@ -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, and you've got a deal.",
() => "Now we're talking. That's a fair price — 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,7 +174,9 @@ 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 }),
);
});
}
@ -184,14 +186,16 @@ 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++) {
@ -216,7 +220,9 @@ 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);
}
@ -253,7 +259,9 @@ 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(
@ -273,9 +281,11 @@ 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 │");
@ -287,16 +297,26 @@ 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;
@ -307,7 +327,12 @@ 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.",
),
},
{

View file

@ -1,13 +0,0 @@
import { describe, expect, it } from "vitest";
import { VoiceSubmissions } from "@/collections/voiceover/VoiceSubmissions";
describe("VoiceSubmissions upload config", () => {
// Regression guard: Payload content-sniffs uploads (file-type) and reports
// browser-recorded WebM takes as `video/webm`, so a MIME allowlist rejects
// every in-browser recording. The collection must accept any audio format;
// the submit server action is the audio/* gate instead.
it("accepts any standard audio format (no MIME allowlist)", () => {
expect(VoiceSubmissions.upload).toBe(true);
});
});