feat(missions): daily ready-check reminders for unfinished side ops
Five days before a player-created side op starts, if it is not yet Ready, the creator gets one in-app notification per day (via mission-tick) plus one Discord DM per day (new bot service). Per-server-local-day dedupe via hidden readyReminderWebSentAt/readyReminderDmSentAt markers. The ready-reminder type is excluded from the notification bridge to avoid double-DMing. Markers are nulled on clone.
This commit is contained in:
parent
ffb2c2c5d9
commit
d757c58c5d
12 changed files with 23682 additions and 88 deletions
|
|
@ -8,6 +8,7 @@ export interface BotConfig {
|
||||||
attendancePollMs: number;
|
attendancePollMs: number;
|
||||||
notificationPollMs: number;
|
notificationPollMs: number;
|
||||||
evaluationPollMs: number;
|
evaluationPollMs: number;
|
||||||
|
readyReminderPollMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const required = (name: string): string => {
|
const required = (name: string): string => {
|
||||||
|
|
@ -48,4 +49,5 @@ export const botConfig: BotConfig = {
|
||||||
attendancePollMs: pollMs("DISCORD_ATTENDANCE_POLL_MS", 60_000),
|
attendancePollMs: pollMs("DISCORD_ATTENDANCE_POLL_MS", 60_000),
|
||||||
notificationPollMs: pollMs("DISCORD_NOTIFICATION_POLL_MS", 20_000),
|
notificationPollMs: pollMs("DISCORD_NOTIFICATION_POLL_MS", 20_000),
|
||||||
evaluationPollMs: pollMs("DISCORD_EVALUATION_POLL_MS", 60_000),
|
evaluationPollMs: pollMs("DISCORD_EVALUATION_POLL_MS", 60_000),
|
||||||
|
readyReminderPollMs: pollMs("DISCORD_READY_REMINDER_POLL_MS", 60_000),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { start as startMissionEmbeds } from "./missionEmbeds";
|
||||||
import { start as startNotificationBridge } from "./notificationBridge";
|
import { start as startNotificationBridge } from "./notificationBridge";
|
||||||
import { start as startTransferRequests } from "./transferRequests";
|
import { start as startTransferRequests } from "./transferRequests";
|
||||||
import { start as startEvaluationReminders } from "./evaluationReminders";
|
import { start as startEvaluationReminders } from "./evaluationReminders";
|
||||||
|
import { start as startReadyReminders } from "./readyReminders";
|
||||||
|
|
||||||
export type ServiceStarter = (client: Client, payload: Payload) => Promise<void> | void;
|
export type ServiceStarter = (client: Client, payload: Payload) => Promise<void> | void;
|
||||||
|
|
||||||
|
|
@ -12,4 +13,5 @@ export const services: ServiceStarter[] = [
|
||||||
startNotificationBridge,
|
startNotificationBridge,
|
||||||
startTransferRequests,
|
startTransferRequests,
|
||||||
startEvaluationReminders,
|
startEvaluationReminders,
|
||||||
|
startReadyReminders,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,19 @@ import type { Payload } from "payload";
|
||||||
import type { User, UserNotification } from "@/payload-types";
|
import type { User, UserNotification } from "@/payload-types";
|
||||||
import { botConfig } from "@/bot/config";
|
import { botConfig } from "@/bot/config";
|
||||||
import { TRANSFER_NOTIFICATION_TYPES } from "@/lib/transfers/notificationTypes";
|
import { TRANSFER_NOTIFICATION_TYPES } from "@/lib/transfers/notificationTypes";
|
||||||
|
import { READY_REMINDER_NOTIFICATION_TYPE } from "@/lib/intelligence/readyReminders";
|
||||||
|
|
||||||
const MAX_SENDS_PER_TICK = 5;
|
const MAX_SENDS_PER_TICK = 5;
|
||||||
const SEND_DELAY_MS = 300;
|
const SEND_DELAY_MS = 300;
|
||||||
const EMBED_COLOR = 0x3b82f6;
|
const EMBED_COLOR = 0x3b82f6;
|
||||||
|
|
||||||
// Transfer notifications are delivered by the dedicated transfer bot service
|
// Transfer notifications are delivered by the dedicated transfer bot service
|
||||||
// (Phase 2), not this generic bridge. Exclude exactly these types.
|
// (Phase 2), not this generic bridge. Ready reminders are delivered by the
|
||||||
const NON_BRIDGED_TYPES = new Set<string>(Object.values(TRANSFER_NOTIFICATION_TYPES));
|
// dedicated readyReminders DM service. Exclude exactly these types.
|
||||||
|
const NON_BRIDGED_TYPES = new Set<string>([
|
||||||
|
...Object.values(TRANSFER_NOTIFICATION_TYPES),
|
||||||
|
READY_REMINDER_NOTIFICATION_TYPE,
|
||||||
|
]);
|
||||||
|
|
||||||
let cursor: number | null = null;
|
let cursor: number | null = null;
|
||||||
|
|
||||||
|
|
|
||||||
130
src/bot/services/readyReminders.ts
Normal file
130
src/bot/services/readyReminders.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { type Client, EmbedBuilder } from "discord.js";
|
||||||
|
import type { Payload } from "payload";
|
||||||
|
import type { Mission, User } from "@/payload-types";
|
||||||
|
import { botConfig } from "@/bot/config";
|
||||||
|
import {
|
||||||
|
READY_REMINDER_LEAD_DAYS,
|
||||||
|
daysUntil,
|
||||||
|
getReadyReminderMissions,
|
||||||
|
isReminderDue,
|
||||||
|
missionTitle,
|
||||||
|
} from "@/lib/intelligence/readyReminders";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ready-reminder service: starting READY_REMINDER_LEAD_DAYS days before a
|
||||||
|
* player-scheduled side op, the op's creator gets a once-per-day DM nudging
|
||||||
|
* them to finish the op and mark it "Ready" — the status that posts the
|
||||||
|
* roll-call, locks in details, and delivers the mission hooks.
|
||||||
|
*
|
||||||
|
* The DM deliberately ignores `preferences.discord.enabled` (the notification
|
||||||
|
* bridge opt-in): that toggle defaults to false and is not yet exposed in the
|
||||||
|
* web UI, so honoring it here would silently DM nobody. This is a dedicated
|
||||||
|
* creator-prompt flow, not a forwarding of the generic notification stream
|
||||||
|
* (the web half of each reminder is excluded from the bridge via
|
||||||
|
* NON_BRIDGED_TYPES). A per-day marker on the mission (readyReminderDmSentAt)
|
||||||
|
* keeps the cadence to one DM per day.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EMBED_COLOR = 0xf59e0b;
|
||||||
|
const MISSIONS_PER_TICK = 10;
|
||||||
|
const SEND_DELAY_MS = 300;
|
||||||
|
|
||||||
|
let reconcileRunning = false;
|
||||||
|
|
||||||
|
const errorMessage = (error: unknown): string =>
|
||||||
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
|
|
||||||
|
const creatorIdOf = (mission: Mission): number | null =>
|
||||||
|
typeof mission.createdBy === "object" ? (mission.createdBy?.id ?? null) : (mission.createdBy ?? null);
|
||||||
|
|
||||||
|
const renderReminderEmbed = (mission: Mission, now: Date): EmbedBuilder => {
|
||||||
|
const days = daysUntil(mission.classification.startDateTime, now);
|
||||||
|
const when = days === 0 ? "today" : days === 1 ? "tomorrow" : `in ${days} days`;
|
||||||
|
return new EmbedBuilder()
|
||||||
|
.setTitle(`⏰ Ready check: ${missionTitle(mission)}`)
|
||||||
|
.setDescription(
|
||||||
|
`Your op starts **${when}** and it isn't marked **Ready** yet.\n\n` +
|
||||||
|
"Finish up the operation and set the mission status to **Ready** so the roll-call can be posted, the details can be locked in, and all the proper mission hooks can be delivered.",
|
||||||
|
)
|
||||||
|
.setColor(EMBED_COLOR)
|
||||||
|
.addFields({
|
||||||
|
name: "Open the op",
|
||||||
|
value: `${botConfig.appUrl}/intelligence/missions/${mission.id}`,
|
||||||
|
})
|
||||||
|
.setFooter({ text: "You'll get this reminder once a day until the op is Ready." });
|
||||||
|
};
|
||||||
|
|
||||||
|
const processMission = async (
|
||||||
|
client: Client,
|
||||||
|
payload: Payload,
|
||||||
|
mission: Mission,
|
||||||
|
now: Date,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
const creatorId = creatorIdOf(mission);
|
||||||
|
if (!creatorId) return false;
|
||||||
|
|
||||||
|
const creator = (await payload.findByID({
|
||||||
|
collection: "users",
|
||||||
|
id: creatorId,
|
||||||
|
depth: 0,
|
||||||
|
overrideAccess: true,
|
||||||
|
})) as unknown as User;
|
||||||
|
if (!creator.discordId) return false;
|
||||||
|
|
||||||
|
const target = await client.users.fetch(creator.discordId);
|
||||||
|
await target.send({ embeds: [renderReminderEmbed(mission, now)] });
|
||||||
|
|
||||||
|
await payload.update({
|
||||||
|
collection: "missions",
|
||||||
|
id: mission.id,
|
||||||
|
data: { readyReminderDmSentAt: now.toISOString() },
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
payload.logger.info(
|
||||||
|
`[Bot] Sent ready reminder DM for mission #${mission.id} (${mission.name}) to user #${creatorId}.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReconcileSummary {
|
||||||
|
processed: number;
|
||||||
|
sent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reconcile = async (client: Client, payload: Payload): Promise<ReconcileSummary> => {
|
||||||
|
if (reconcileRunning) return { processed: 0, sent: 0 };
|
||||||
|
reconcileRunning = true;
|
||||||
|
let processed = 0;
|
||||||
|
let sent = 0;
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const missions = (await getReadyReminderMissions(payload, now)).slice(0, MISSIONS_PER_TICK);
|
||||||
|
for (const mission of missions) {
|
||||||
|
if (!isReminderDue(mission.readyReminderDmSentAt, now)) continue;
|
||||||
|
processed += 1;
|
||||||
|
try {
|
||||||
|
if (await processMission(client, payload, mission, now)) {
|
||||||
|
sent += 1;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, SEND_DELAY_MS));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
payload.logger.error(
|
||||||
|
`[Bot] Ready reminder failed for mission #${mission.id}: ${errorMessage(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
payload.logger.error(`[Bot] Ready reminder reconcile failed: ${errorMessage(error)}`);
|
||||||
|
} finally {
|
||||||
|
reconcileRunning = false;
|
||||||
|
}
|
||||||
|
return { processed, sent };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const start = (client: Client, payload: Payload): void => {
|
||||||
|
payload.logger.info("[Bot] Ready reminder service started");
|
||||||
|
void reconcile(client, payload);
|
||||||
|
setInterval(() => {
|
||||||
|
void reconcile(client, payload);
|
||||||
|
}, botConfig.readyReminderPollMs);
|
||||||
|
};
|
||||||
|
|
@ -216,6 +216,24 @@ export const Missions: CollectionConfig = {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "readyReminderWebSentAt",
|
||||||
|
type: "date",
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Server-local day the last web 'not Ready' reminder notification was sent to the op creator (managed by the mission tick, once per day).",
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "readyReminderDmSentAt",
|
||||||
|
type: "date",
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Server-local day the last 'not Ready' reminder DM was sent to the op creator (managed by the Discord bot, once per day).",
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "createdBy",
|
name: "createdBy",
|
||||||
type: "relationship",
|
type: "relationship",
|
||||||
|
|
|
||||||
138
src/lib/intelligence/readyReminders.ts
Normal file
138
src/lib/intelligence/readyReminders.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
import type { Payload } from "payload";
|
||||||
|
import type { Mission } from "@/payload-types";
|
||||||
|
import { notifyUser } from "@/lib/notifications";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ready-reminder system for player-scheduled side ops.
|
||||||
|
*
|
||||||
|
* Starting READY_REMINDER_LEAD_DAYS days before an op's scheduled start, the
|
||||||
|
* creator gets a once-per-day reminder (web notification + Discord DM) to
|
||||||
|
* finish the op and mark it "Ready" — the status that triggers the roll-call
|
||||||
|
* embed, locks in details, and delivers the mission hooks. Reminders stop the
|
||||||
|
* moment the op is Ready (or cancelled/completed/active), and a marker field
|
||||||
|
* records the last server-local day each channel was sent so the cadence is
|
||||||
|
* exactly one reminder per day per channel.
|
||||||
|
*
|
||||||
|
* Scope: only ops with a `createdBy` (player-scheduled via the Friday Ops
|
||||||
|
* calendar — always `operationType: "side"`). Staff/admin-created missions are
|
||||||
|
* never reminded.
|
||||||
|
*
|
||||||
|
* Shared by the mission-tick bin (web notifications) and the Discord bot's
|
||||||
|
* readyReminders service (DMs) — keep this module free of discord.js imports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** How many days before the op the daily reminders begin. */
|
||||||
|
export const READY_REMINDER_LEAD_DAYS = 5;
|
||||||
|
|
||||||
|
/** Notification type used for the web-app reminder (labels + bridge exclusion). */
|
||||||
|
export const READY_REMINDER_NOTIFICATION_TYPE = "mission:ready-reminder";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statuses that still need the creator's attention. "Ready" is the committed
|
||||||
|
* state the reminders push toward; Active/Completed/Cancelled are terminal or
|
||||||
|
* already underway.
|
||||||
|
*/
|
||||||
|
export const READY_REMINDER_ELIGIBLE_STATUSES = ["Concept", "Planning", "Scheduled"] as const;
|
||||||
|
|
||||||
|
export const missionTitle = (mission: Mission): string =>
|
||||||
|
mission.codeName ? `${mission.name} (${mission.codeName})` : mission.name;
|
||||||
|
|
||||||
|
/** Server-local YYYY-MM-DD key used for the once-per-day cadence. */
|
||||||
|
export const dayKey = (date: Date): string =>
|
||||||
|
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(
|
||||||
|
date.getDate(),
|
||||||
|
).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A channel is due when it has never sent, or last sent on a previous
|
||||||
|
* server-local day. Day-based (not 24h-based) so retries within the same day
|
||||||
|
* stay deduped, and a status flapping Ready → Scheduled resumes reminders the
|
||||||
|
* next day instead of being suppressed forever.
|
||||||
|
*/
|
||||||
|
export const isReminderDue = (marker: string | null | undefined, now: Date): boolean => {
|
||||||
|
if (!marker) return true;
|
||||||
|
const markerDate = new Date(marker);
|
||||||
|
if (Number.isNaN(markerDate.getTime())) return true;
|
||||||
|
return dayKey(markerDate) !== dayKey(now);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const daysUntil = (startIso: string, now: Date): number =>
|
||||||
|
Math.max(0, Math.ceil((new Date(startIso).getTime() - now.getTime()) / 86_400_000));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ops due for reminders: player-created (createdBy set), still not Ready /
|
||||||
|
* terminal, and starting within the lead window (future start, at most
|
||||||
|
* READY_REMINDER_LEAD_DAYS days out). Marker filtering is done per-channel by
|
||||||
|
* the caller via isReminderDue — the "not sent today" condition can't be
|
||||||
|
* expressed in a single where clause.
|
||||||
|
*/
|
||||||
|
export const getReadyReminderMissions = async (payload: Payload, now = new Date()): Promise<Mission[]> => {
|
||||||
|
const nowIso = now.toISOString();
|
||||||
|
const windowEndIso = new Date(
|
||||||
|
now.getTime() + READY_REMINDER_LEAD_DAYS * 86_400_000,
|
||||||
|
).toISOString();
|
||||||
|
const res = await payload.find({
|
||||||
|
collection: "missions",
|
||||||
|
overrideAccess: true,
|
||||||
|
depth: 0,
|
||||||
|
sort: "classification.startDateTime",
|
||||||
|
limit: 50,
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{ createdBy: { exists: true } },
|
||||||
|
{ "ownershipAndStatus.status": { in: [...READY_REMINDER_ELIGIBLE_STATUSES] } },
|
||||||
|
{ "classification.startDateTime": { greater_than: nowIso } },
|
||||||
|
{ "classification.startDateTime": { less_than_equal: windowEndIso } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.docs as unknown as Mission[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReadyReminderSummary {
|
||||||
|
missions: number;
|
||||||
|
notified: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web sweep: one in-app notification per due mission per server-local day.
|
||||||
|
* Called from the mission-tick bin. Respects each user's notification mutes
|
||||||
|
* (notifyUser handles suppression) and stamps the per-day marker.
|
||||||
|
*/
|
||||||
|
export const sendWebReadyReminders = async (
|
||||||
|
payload: Payload,
|
||||||
|
now = new Date(),
|
||||||
|
): Promise<ReadyReminderSummary> => {
|
||||||
|
const missions = await getReadyReminderMissions(payload, now);
|
||||||
|
let notified = 0;
|
||||||
|
for (const mission of missions) {
|
||||||
|
if (!isReminderDue(mission.readyReminderWebSentAt, now)) continue;
|
||||||
|
if (mission.createdBy == null) continue;
|
||||||
|
const creatorId =
|
||||||
|
typeof mission.createdBy === "object" ? mission.createdBy.id : mission.createdBy;
|
||||||
|
if (!creatorId) continue;
|
||||||
|
|
||||||
|
const days = daysUntil(mission.classification.startDateTime, now);
|
||||||
|
const when =
|
||||||
|
days === 0
|
||||||
|
? "today"
|
||||||
|
: days === 1
|
||||||
|
? "tomorrow"
|
||||||
|
: `in ${days} days`;
|
||||||
|
await notifyUser(payload, {
|
||||||
|
userId: creatorId,
|
||||||
|
type: READY_REMINDER_NOTIFICATION_TYPE,
|
||||||
|
title: `Ready check: ${missionTitle(mission)}`,
|
||||||
|
message: `Your op starts ${when}. Finish up the details and set the mission status to Ready so the roll-call can be posted and the mission hooks delivered.`,
|
||||||
|
link: `/intelligence/missions/${mission.id}`,
|
||||||
|
});
|
||||||
|
await payload.update({
|
||||||
|
collection: "missions",
|
||||||
|
id: mission.id,
|
||||||
|
data: { readyReminderWebSentAt: now.toISOString() },
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
notified += 1;
|
||||||
|
}
|
||||||
|
return { missions: missions.length, notified };
|
||||||
|
};
|
||||||
|
|
@ -91,6 +91,8 @@ export function notificationLabel(type?: string | null): string {
|
||||||
return "New reply";
|
return "New reply";
|
||||||
case "ticket:status":
|
case "ticket:status":
|
||||||
return "Ticket updated";
|
return "Ticket updated";
|
||||||
|
case "mission:ready-reminder":
|
||||||
|
return "Ready reminder";
|
||||||
case "assignment-transfer:request":
|
case "assignment-transfer:request":
|
||||||
return "Transfer request";
|
return "Transfer request";
|
||||||
case "assignment-transfer:approved":
|
case "assignment-transfer:approved":
|
||||||
|
|
|
||||||
23260
src/migrations/20260907_090245_add_mission_ready_reminder_fields.json
Normal file
23260
src/migrations/20260907_090245_add_mission_ready_reminder_fields.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "missions" ADD COLUMN "ready_reminder_web_sent_at" timestamp(3) with time zone;
|
||||||
|
ALTER TABLE "missions" ADD COLUMN "ready_reminder_dm_sent_at" timestamp(3) with time zone;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "missions" DROP COLUMN "ready_reminder_web_sent_at";
|
||||||
|
ALTER TABLE "missions" DROP COLUMN "ready_reminder_dm_sent_at";`)
|
||||||
|
}
|
||||||
|
|
@ -1,261 +1,267 @@
|
||||||
import * as migration_20260820_004157 from "./20260820_004157";
|
import * as migration_20260820_004157 from './20260820_004157';
|
||||||
import * as migration_20260825_220000 from "./20260825_220000";
|
import * as migration_20260825_220000 from './20260825_220000';
|
||||||
import * as migration_20260826_050000_repair_shims_auxiliary_tables from "./20260826_050000_repair_shims_auxiliary_tables";
|
import * as migration_20260826_050000_repair_shims_auxiliary_tables from './20260826_050000_repair_shims_auxiliary_tables';
|
||||||
import * as migration_20260826_060000_add_game_servers_event_target from "./20260826_060000_add_game_servers_event_target";
|
import * as migration_20260826_060000_add_game_servers_event_target from './20260826_060000_add_game_servers_event_target';
|
||||||
import * as migration_20260827_005757 from "./20260827_005757";
|
import * as migration_20260827_005757 from './20260827_005757';
|
||||||
import * as migration_20260827_022334_add_eod_minesweeper_best_flags from "./20260827_022334_add_eod_minesweeper_best_flags";
|
import * as migration_20260827_022334_add_eod_minesweeper_best_flags from './20260827_022334_add_eod_minesweeper_best_flags';
|
||||||
import * as migration_20260827_213857_add_evaluations_collection from "./20260827_213857_add_evaluations_collection";
|
import * as migration_20260827_213857_add_evaluations_collection from './20260827_213857_add_evaluations_collection';
|
||||||
import * as migration_20260827_233224_add_mission_completed_at from "./20260827_233224_add_mission_completed_at";
|
import * as migration_20260827_233224_add_mission_completed_at from './20260827_233224_add_mission_completed_at';
|
||||||
import * as migration_20260828_011500_add_background_texture_preference from "./20260828_011500_add_background_texture_preference";
|
import * as migration_20260828_011500_add_background_texture_preference from './20260828_011500_add_background_texture_preference';
|
||||||
import * as migration_20260828_041615_add_campaign_mode from "./20260828_041615_add_campaign_mode";
|
import * as migration_20260828_041615_add_campaign_mode from './20260828_041615_add_campaign_mode';
|
||||||
import * as migration_20260828_044506 from "./20260828_044506";
|
import * as migration_20260828_044506 from './20260828_044506';
|
||||||
import * as migration_20260831_142503 from "./20260831_142503";
|
import * as migration_20260831_142503 from './20260831_142503';
|
||||||
import * as migration_20260831_180233_assignment_transfers from "./20260831_180233_assignment_transfers";
|
import * as migration_20260831_180233_assignment_transfers from './20260831_180233_assignment_transfers';
|
||||||
import * as migration_20260901_014006_add_assignment_transfer_event_target from "./20260901_014006_add_assignment_transfer_event_target";
|
import * as migration_20260901_014006_add_assignment_transfer_event_target from './20260901_014006_add_assignment_transfer_event_target';
|
||||||
import * as migration_20260901_213108_add_ticket_votes_and_edit_history from "./20260901_213108_add_ticket_votes_and_edit_history";
|
import * as migration_20260901_213108_add_ticket_votes_and_edit_history from './20260901_213108_add_ticket_votes_and_edit_history';
|
||||||
import * as migration_20260901_222200_add_admin_page_permissions from "./20260901_222200_add_admin_page_permissions";
|
import * as migration_20260901_222200_add_admin_page_permissions from './20260901_222200_add_admin_page_permissions';
|
||||||
import * as migration_20260902_195336_add_medal_backgrounds from "./20260902_195336_add_medal_backgrounds";
|
import * as migration_20260902_195336_add_medal_backgrounds from './20260902_195336_add_medal_backgrounds';
|
||||||
import * as migration_20260903_035544 from "./20260903_035544";
|
import * as migration_20260903_035544 from './20260903_035544';
|
||||||
import * as migration_20260903_044047 from "./20260903_044047";
|
import * as migration_20260903_044047 from './20260903_044047';
|
||||||
import * as migration_20260903_053419 from "./20260903_053419";
|
import * as migration_20260903_053419 from './20260903_053419';
|
||||||
import * as migration_20260903_060000_add_radio_mode_stats from "./20260903_060000_add_radio_mode_stats";
|
import * as migration_20260903_060000_add_radio_mode_stats from './20260903_060000_add_radio_mode_stats';
|
||||||
import * as migration_20260903_061000_add_radio_incorrect_counts from "./20260903_061000_add_radio_incorrect_counts";
|
import * as migration_20260903_061000_add_radio_incorrect_counts from './20260903_061000_add_radio_incorrect_counts';
|
||||||
import * as migration_20260903_062000_add_aim_trainer_stats from "./20260903_062000_add_aim_trainer_stats";
|
import * as migration_20260903_062000_add_aim_trainer_stats from './20260903_062000_add_aim_trainer_stats';
|
||||||
import * as migration_20260904_230838_add_wiki_collections from "./20260904_230838_add_wiki_collections";
|
import * as migration_20260904_230838_add_wiki_collections from './20260904_230838_add_wiki_collections';
|
||||||
import * as migration_20260905_183940_base_management from "./20260905_183940_base_management";
|
import * as migration_20260905_183940_base_management from './20260905_183940_base_management';
|
||||||
import * as migration_20260905_195456_labor_headcount from "./20260905_195456_labor_headcount";
|
import * as migration_20260905_195456_labor_headcount from './20260905_195456_labor_headcount';
|
||||||
import * as migration_20260905_210310_labor_classifications from "./20260905_210310_labor_classifications";
|
import * as migration_20260905_210310_labor_classifications from './20260905_210310_labor_classifications';
|
||||||
import * as migration_20260905_213824 from "./20260905_213824";
|
import * as migration_20260905_213824 from './20260905_213824';
|
||||||
import * as migration_20260905_215658 from "./20260905_215658";
|
import * as migration_20260905_215658 from './20260905_215658';
|
||||||
import * as migration_20260906_045502 from "./20260906_045502";
|
import * as migration_20260906_045502 from './20260906_045502';
|
||||||
import * as migration_20260906_050900_add_evaluation_reminders_field from "./20260906_050900_add_evaluation_reminders_field";
|
import * as migration_20260906_050900_add_evaluation_reminders_field from './20260906_050900_add_evaluation_reminders_field';
|
||||||
import * as migration_20260906_061451_add_wiki_template_css from "./20260906_061451_add_wiki_template_css";
|
import * as migration_20260906_061451_add_wiki_template_css from './20260906_061451_add_wiki_template_css';
|
||||||
import * as migration_20260906_063753_add_personal_excerpt_richtext from "./20260906_063753_add_personal_excerpt_richtext";
|
import * as migration_20260906_063753_add_personal_excerpt_richtext from './20260906_063753_add_personal_excerpt_richtext';
|
||||||
import * as migration_20260906_065405_add_wiki_template_html from "./20260906_065405_add_wiki_template_html";
|
import * as migration_20260906_065405_add_wiki_template_html from './20260906_065405_add_wiki_template_html';
|
||||||
import * as migration_20260906_100908_add_market_economy_state from "./20260906_100908_add_market_economy_state";
|
import * as migration_20260906_100908_add_market_economy_state from './20260906_100908_add_market_economy_state';
|
||||||
import * as migration_20260906_165837_add_market_state_event_target from "./20260906_165837_add_market_state_event_target";
|
import * as migration_20260906_165837_add_market_state_event_target from './20260906_165837_add_market_state_event_target';
|
||||||
import * as migration_20260906_202925_add_voice_submissions from "./20260906_202925_add_voice_submissions";
|
import * as migration_20260906_202925_add_voice_submissions from './20260906_202925_add_voice_submissions';
|
||||||
import * as migration_20260907_012326_add_missions_created_by from "./20260907_012326_add_missions_created_by";
|
import * as migration_20260907_012326_add_missions_created_by from './20260907_012326_add_missions_created_by';
|
||||||
import * as migration_20260907_013724_make_missions_campaign_optional from "./20260907_013724_make_missions_campaign_optional";
|
import * as migration_20260907_013724_make_missions_campaign_optional from './20260907_013724_make_missions_campaign_optional';
|
||||||
import * as migration_20260907_041051_add_voice_script_spoiler from "./20260907_041051_add_voice_script_spoiler";
|
import * as migration_20260907_041051_add_voice_script_spoiler from './20260907_041051_add_voice_script_spoiler';
|
||||||
import * as migration_20260907_043339_add_voice_script_description from "./20260907_043339_add_voice_script_description";
|
import * as migration_20260907_043339_add_voice_script_description from './20260907_043339_add_voice_script_description';
|
||||||
import * as migration_20260907_044934_add_missions_discord_modlist_hash from "./20260907_044934_add_missions_discord_modlist_hash";
|
import * as migration_20260907_044934_add_missions_discord_modlist_hash from './20260907_044934_add_missions_discord_modlist_hash';
|
||||||
import * as migration_20260907_083010_add_voice_submission_description from "./20260907_083010_add_voice_submission_description";
|
import * as migration_20260907_083010_add_voice_submission_description from './20260907_083010_add_voice_submission_description';
|
||||||
|
import * as migration_20260907_090245_add_mission_ready_reminder_fields from './20260907_090245_add_mission_ready_reminder_fields';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
up: migration_20260820_004157.up,
|
up: migration_20260820_004157.up,
|
||||||
down: migration_20260820_004157.down,
|
down: migration_20260820_004157.down,
|
||||||
name: "20260820_004157",
|
name: '20260820_004157',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260825_220000.up,
|
up: migration_20260825_220000.up,
|
||||||
down: migration_20260825_220000.down,
|
down: migration_20260825_220000.down,
|
||||||
name: "20260825_220000",
|
name: '20260825_220000',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260826_050000_repair_shims_auxiliary_tables.up,
|
up: migration_20260826_050000_repair_shims_auxiliary_tables.up,
|
||||||
down: migration_20260826_050000_repair_shims_auxiliary_tables.down,
|
down: migration_20260826_050000_repair_shims_auxiliary_tables.down,
|
||||||
name: "20260826_050000_repair_shims_auxiliary_tables",
|
name: '20260826_050000_repair_shims_auxiliary_tables',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260826_060000_add_game_servers_event_target.up,
|
up: migration_20260826_060000_add_game_servers_event_target.up,
|
||||||
down: migration_20260826_060000_add_game_servers_event_target.down,
|
down: migration_20260826_060000_add_game_servers_event_target.down,
|
||||||
name: "20260826_060000_add_game_servers_event_target",
|
name: '20260826_060000_add_game_servers_event_target',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260827_005757.up,
|
up: migration_20260827_005757.up,
|
||||||
down: migration_20260827_005757.down,
|
down: migration_20260827_005757.down,
|
||||||
name: "20260827_005757",
|
name: '20260827_005757',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260827_022334_add_eod_minesweeper_best_flags.up,
|
up: migration_20260827_022334_add_eod_minesweeper_best_flags.up,
|
||||||
down: migration_20260827_022334_add_eod_minesweeper_best_flags.down,
|
down: migration_20260827_022334_add_eod_minesweeper_best_flags.down,
|
||||||
name: "20260827_022334_add_eod_minesweeper_best_flags",
|
name: '20260827_022334_add_eod_minesweeper_best_flags',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260827_213857_add_evaluations_collection.up,
|
up: migration_20260827_213857_add_evaluations_collection.up,
|
||||||
down: migration_20260827_213857_add_evaluations_collection.down,
|
down: migration_20260827_213857_add_evaluations_collection.down,
|
||||||
name: "20260827_213857_add_evaluations_collection",
|
name: '20260827_213857_add_evaluations_collection',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260827_233224_add_mission_completed_at.up,
|
up: migration_20260827_233224_add_mission_completed_at.up,
|
||||||
down: migration_20260827_233224_add_mission_completed_at.down,
|
down: migration_20260827_233224_add_mission_completed_at.down,
|
||||||
name: "20260827_233224_add_mission_completed_at",
|
name: '20260827_233224_add_mission_completed_at',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260828_011500_add_background_texture_preference.up,
|
up: migration_20260828_011500_add_background_texture_preference.up,
|
||||||
down: migration_20260828_011500_add_background_texture_preference.down,
|
down: migration_20260828_011500_add_background_texture_preference.down,
|
||||||
name: "20260828_011500_add_background_texture_preference",
|
name: '20260828_011500_add_background_texture_preference',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260828_041615_add_campaign_mode.up,
|
up: migration_20260828_041615_add_campaign_mode.up,
|
||||||
down: migration_20260828_041615_add_campaign_mode.down,
|
down: migration_20260828_041615_add_campaign_mode.down,
|
||||||
name: "20260828_041615_add_campaign_mode",
|
name: '20260828_041615_add_campaign_mode',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260828_044506.up,
|
up: migration_20260828_044506.up,
|
||||||
down: migration_20260828_044506.down,
|
down: migration_20260828_044506.down,
|
||||||
name: "20260828_044506",
|
name: '20260828_044506',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260831_142503.up,
|
up: migration_20260831_142503.up,
|
||||||
down: migration_20260831_142503.down,
|
down: migration_20260831_142503.down,
|
||||||
name: "20260831_142503",
|
name: '20260831_142503',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260831_180233_assignment_transfers.up,
|
up: migration_20260831_180233_assignment_transfers.up,
|
||||||
down: migration_20260831_180233_assignment_transfers.down,
|
down: migration_20260831_180233_assignment_transfers.down,
|
||||||
name: "20260831_180233_assignment_transfers",
|
name: '20260831_180233_assignment_transfers',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260901_014006_add_assignment_transfer_event_target.up,
|
up: migration_20260901_014006_add_assignment_transfer_event_target.up,
|
||||||
down: migration_20260901_014006_add_assignment_transfer_event_target.down,
|
down: migration_20260901_014006_add_assignment_transfer_event_target.down,
|
||||||
name: "20260901_014006_add_assignment_transfer_event_target",
|
name: '20260901_014006_add_assignment_transfer_event_target',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260901_213108_add_ticket_votes_and_edit_history.up,
|
up: migration_20260901_213108_add_ticket_votes_and_edit_history.up,
|
||||||
down: migration_20260901_213108_add_ticket_votes_and_edit_history.down,
|
down: migration_20260901_213108_add_ticket_votes_and_edit_history.down,
|
||||||
name: "20260901_213108_add_ticket_votes_and_edit_history",
|
name: '20260901_213108_add_ticket_votes_and_edit_history',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260901_222200_add_admin_page_permissions.up,
|
up: migration_20260901_222200_add_admin_page_permissions.up,
|
||||||
down: migration_20260901_222200_add_admin_page_permissions.down,
|
down: migration_20260901_222200_add_admin_page_permissions.down,
|
||||||
name: "20260901_222200_add_admin_page_permissions",
|
name: '20260901_222200_add_admin_page_permissions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260902_195336_add_medal_backgrounds.up,
|
up: migration_20260902_195336_add_medal_backgrounds.up,
|
||||||
down: migration_20260902_195336_add_medal_backgrounds.down,
|
down: migration_20260902_195336_add_medal_backgrounds.down,
|
||||||
name: "20260902_195336_add_medal_backgrounds",
|
name: '20260902_195336_add_medal_backgrounds',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_035544.up,
|
up: migration_20260903_035544.up,
|
||||||
down: migration_20260903_035544.down,
|
down: migration_20260903_035544.down,
|
||||||
name: "20260903_035544",
|
name: '20260903_035544',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_044047.up,
|
up: migration_20260903_044047.up,
|
||||||
down: migration_20260903_044047.down,
|
down: migration_20260903_044047.down,
|
||||||
name: "20260903_044047",
|
name: '20260903_044047',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_053419.up,
|
up: migration_20260903_053419.up,
|
||||||
down: migration_20260903_053419.down,
|
down: migration_20260903_053419.down,
|
||||||
name: "20260903_053419",
|
name: '20260903_053419',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_060000_add_radio_mode_stats.up,
|
up: migration_20260903_060000_add_radio_mode_stats.up,
|
||||||
down: migration_20260903_060000_add_radio_mode_stats.down,
|
down: migration_20260903_060000_add_radio_mode_stats.down,
|
||||||
name: "20260903_060000_add_radio_mode_stats",
|
name: '20260903_060000_add_radio_mode_stats',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_061000_add_radio_incorrect_counts.up,
|
up: migration_20260903_061000_add_radio_incorrect_counts.up,
|
||||||
down: migration_20260903_061000_add_radio_incorrect_counts.down,
|
down: migration_20260903_061000_add_radio_incorrect_counts.down,
|
||||||
name: "20260903_061000_add_radio_incorrect_counts",
|
name: '20260903_061000_add_radio_incorrect_counts',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260903_062000_add_aim_trainer_stats.up,
|
up: migration_20260903_062000_add_aim_trainer_stats.up,
|
||||||
down: migration_20260903_062000_add_aim_trainer_stats.down,
|
down: migration_20260903_062000_add_aim_trainer_stats.down,
|
||||||
name: "20260903_062000_add_aim_trainer_stats",
|
name: '20260903_062000_add_aim_trainer_stats',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260904_230838_add_wiki_collections.up,
|
up: migration_20260904_230838_add_wiki_collections.up,
|
||||||
down: migration_20260904_230838_add_wiki_collections.down,
|
down: migration_20260904_230838_add_wiki_collections.down,
|
||||||
name: "20260904_230838_add_wiki_collections",
|
name: '20260904_230838_add_wiki_collections',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260905_183940_base_management.up,
|
up: migration_20260905_183940_base_management.up,
|
||||||
down: migration_20260905_183940_base_management.down,
|
down: migration_20260905_183940_base_management.down,
|
||||||
name: "20260905_183940_base_management",
|
name: '20260905_183940_base_management',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260905_195456_labor_headcount.up,
|
up: migration_20260905_195456_labor_headcount.up,
|
||||||
down: migration_20260905_195456_labor_headcount.down,
|
down: migration_20260905_195456_labor_headcount.down,
|
||||||
name: "20260905_195456_labor_headcount",
|
name: '20260905_195456_labor_headcount',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260905_210310_labor_classifications.up,
|
up: migration_20260905_210310_labor_classifications.up,
|
||||||
down: migration_20260905_210310_labor_classifications.down,
|
down: migration_20260905_210310_labor_classifications.down,
|
||||||
name: "20260905_210310_labor_classifications",
|
name: '20260905_210310_labor_classifications',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260905_213824.up,
|
up: migration_20260905_213824.up,
|
||||||
down: migration_20260905_213824.down,
|
down: migration_20260905_213824.down,
|
||||||
name: "20260905_213824",
|
name: '20260905_213824',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260905_215658.up,
|
up: migration_20260905_215658.up,
|
||||||
down: migration_20260905_215658.down,
|
down: migration_20260905_215658.down,
|
||||||
name: "20260905_215658",
|
name: '20260905_215658',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_045502.up,
|
up: migration_20260906_045502.up,
|
||||||
down: migration_20260906_045502.down,
|
down: migration_20260906_045502.down,
|
||||||
name: "20260906_045502",
|
name: '20260906_045502',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_050900_add_evaluation_reminders_field.up,
|
up: migration_20260906_050900_add_evaluation_reminders_field.up,
|
||||||
down: migration_20260906_050900_add_evaluation_reminders_field.down,
|
down: migration_20260906_050900_add_evaluation_reminders_field.down,
|
||||||
name: "20260906_050900_add_evaluation_reminders_field",
|
name: '20260906_050900_add_evaluation_reminders_field',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_061451_add_wiki_template_css.up,
|
up: migration_20260906_061451_add_wiki_template_css.up,
|
||||||
down: migration_20260906_061451_add_wiki_template_css.down,
|
down: migration_20260906_061451_add_wiki_template_css.down,
|
||||||
name: "20260906_061451_add_wiki_template_css",
|
name: '20260906_061451_add_wiki_template_css',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_063753_add_personal_excerpt_richtext.up,
|
up: migration_20260906_063753_add_personal_excerpt_richtext.up,
|
||||||
down: migration_20260906_063753_add_personal_excerpt_richtext.down,
|
down: migration_20260906_063753_add_personal_excerpt_richtext.down,
|
||||||
name: "20260906_063753_add_personal_excerpt_richtext",
|
name: '20260906_063753_add_personal_excerpt_richtext',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_065405_add_wiki_template_html.up,
|
up: migration_20260906_065405_add_wiki_template_html.up,
|
||||||
down: migration_20260906_065405_add_wiki_template_html.down,
|
down: migration_20260906_065405_add_wiki_template_html.down,
|
||||||
name: "20260906_065405_add_wiki_template_html",
|
name: '20260906_065405_add_wiki_template_html',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_100908_add_market_economy_state.up,
|
up: migration_20260906_100908_add_market_economy_state.up,
|
||||||
down: migration_20260906_100908_add_market_economy_state.down,
|
down: migration_20260906_100908_add_market_economy_state.down,
|
||||||
name: "20260906_100908_add_market_economy_state",
|
name: '20260906_100908_add_market_economy_state',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_165837_add_market_state_event_target.up,
|
up: migration_20260906_165837_add_market_state_event_target.up,
|
||||||
down: migration_20260906_165837_add_market_state_event_target.down,
|
down: migration_20260906_165837_add_market_state_event_target.down,
|
||||||
name: "20260906_165837_add_market_state_event_target",
|
name: '20260906_165837_add_market_state_event_target',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260906_202925_add_voice_submissions.up,
|
up: migration_20260906_202925_add_voice_submissions.up,
|
||||||
down: migration_20260906_202925_add_voice_submissions.down,
|
down: migration_20260906_202925_add_voice_submissions.down,
|
||||||
name: "20260906_202925_add_voice_submissions",
|
name: '20260906_202925_add_voice_submissions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_012326_add_missions_created_by.up,
|
up: migration_20260907_012326_add_missions_created_by.up,
|
||||||
down: migration_20260907_012326_add_missions_created_by.down,
|
down: migration_20260907_012326_add_missions_created_by.down,
|
||||||
name: "20260907_012326_add_missions_created_by",
|
name: '20260907_012326_add_missions_created_by',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_013724_make_missions_campaign_optional.up,
|
up: migration_20260907_013724_make_missions_campaign_optional.up,
|
||||||
down: migration_20260907_013724_make_missions_campaign_optional.down,
|
down: migration_20260907_013724_make_missions_campaign_optional.down,
|
||||||
name: "20260907_013724_make_missions_campaign_optional",
|
name: '20260907_013724_make_missions_campaign_optional',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_041051_add_voice_script_spoiler.up,
|
up: migration_20260907_041051_add_voice_script_spoiler.up,
|
||||||
down: migration_20260907_041051_add_voice_script_spoiler.down,
|
down: migration_20260907_041051_add_voice_script_spoiler.down,
|
||||||
name: "20260907_041051_add_voice_script_spoiler",
|
name: '20260907_041051_add_voice_script_spoiler',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_043339_add_voice_script_description.up,
|
up: migration_20260907_043339_add_voice_script_description.up,
|
||||||
down: migration_20260907_043339_add_voice_script_description.down,
|
down: migration_20260907_043339_add_voice_script_description.down,
|
||||||
name: "20260907_043339_add_voice_script_description",
|
name: '20260907_043339_add_voice_script_description',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_044934_add_missions_discord_modlist_hash.up,
|
up: migration_20260907_044934_add_missions_discord_modlist_hash.up,
|
||||||
down: migration_20260907_044934_add_missions_discord_modlist_hash.down,
|
down: migration_20260907_044934_add_missions_discord_modlist_hash.down,
|
||||||
name: "20260907_044934_add_missions_discord_modlist_hash",
|
name: '20260907_044934_add_missions_discord_modlist_hash',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
up: migration_20260907_083010_add_voice_submission_description.up,
|
up: migration_20260907_083010_add_voice_submission_description.up,
|
||||||
down: migration_20260907_083010_add_voice_submission_description.down,
|
down: migration_20260907_083010_add_voice_submission_description.down,
|
||||||
name: "20260907_083010_add_voice_submission_description",
|
name: '20260907_083010_add_voice_submission_description',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260907_090245_add_mission_ready_reminder_fields.up,
|
||||||
|
down: migration_20260907_090245_add_mission_ready_reminder_fields.down,
|
||||||
|
name: '20260907_090245_add_mission_ready_reminder_fields'
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -2380,6 +2380,14 @@ export interface Mission {
|
||||||
* When evaluation reminder DMs were sent for this mission (managed by the bot, one-shot).
|
* When evaluation reminder DMs were sent for this mission (managed by the bot, one-shot).
|
||||||
*/
|
*/
|
||||||
evaluationRemindersSentAt?: string | null;
|
evaluationRemindersSentAt?: string | null;
|
||||||
|
/**
|
||||||
|
* Server-local day the last web 'not Ready' reminder notification was sent to the op creator (managed by the mission tick, once per day).
|
||||||
|
*/
|
||||||
|
readyReminderWebSentAt?: string | null;
|
||||||
|
/**
|
||||||
|
* Server-local day the last 'not Ready' reminder DM was sent to the op creator (managed by the Discord bot, once per day).
|
||||||
|
*/
|
||||||
|
readyReminderDmSentAt?: string | null;
|
||||||
/**
|
/**
|
||||||
* Player who scheduled this mission via the Friday Ops calendar (ownership + scheduling-cap tracking). Set automatically; leave empty for staff-created missions.
|
* Player who scheduled this mission via the Friday Ops calendar (ownership + scheduling-cap tracking). Set automatically; leave empty for staff-created missions.
|
||||||
*/
|
*/
|
||||||
|
|
@ -4489,6 +4497,8 @@ export interface MissionsSelect<T extends boolean = true> {
|
||||||
discordAttendanceSentAt?: T;
|
discordAttendanceSentAt?: T;
|
||||||
discordModlistHash?: T;
|
discordModlistHash?: T;
|
||||||
evaluationRemindersSentAt?: T;
|
evaluationRemindersSentAt?: T;
|
||||||
|
readyReminderWebSentAt?: T;
|
||||||
|
readyReminderDmSentAt?: T;
|
||||||
createdBy?: T;
|
createdBy?: T;
|
||||||
coverImage?: T;
|
coverImage?: T;
|
||||||
classification?:
|
classification?:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { SanitizedConfig } from "payload";
|
import type { SanitizedConfig } from "payload";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import { autoCompleteMissions } from "@/lib/intelligence/missionLifecycle";
|
import { autoCompleteMissions } from "@/lib/intelligence/missionLifecycle";
|
||||||
|
import { sendWebReadyReminders } from "@/lib/intelligence/readyReminders";
|
||||||
import { createBinLogger, errorMessage } from "./lib/binFileLogger";
|
import { createBinLogger, errorMessage } from "./lib/binFileLogger";
|
||||||
|
|
||||||
export const script = async (config: SanitizedConfig) => {
|
export const script = async (config: SanitizedConfig) => {
|
||||||
|
|
@ -13,6 +14,13 @@ export const script = async (config: SanitizedConfig) => {
|
||||||
const nowIso = new Date().toISOString();
|
const nowIso = new Date().toISOString();
|
||||||
const result = await autoCompleteMissions(payload);
|
const result = await autoCompleteMissions(payload);
|
||||||
|
|
||||||
|
const reminders = await sendWebReadyReminders(payload);
|
||||||
|
if (reminders.notified > 0) {
|
||||||
|
log.info(
|
||||||
|
`[Mission Tick] Sent ${reminders.notified} ready reminder notification(s) across ${reminders.missions} due op(s).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
const notifyUrl = (process.env.APP_URL ?? "http://localhost:3000").replace(/\/$/, "");
|
||||||
const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET;
|
const notifySecret = process.env.GAME_TICK_NOTIFY_SECRET;
|
||||||
if (notifySecret) {
|
if (notifySecret) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue