diff --git a/reset-seq.sql b/reset-seq.sql index 6638e13..da2c028 100644 --- a/reset-seq.sql +++ b/reset-seq.sql @@ -1,8 +1,9 @@ SELECT 'SELECT SETVAL(' || quote_literal(quote_ident(sequence_namespace.nspname) || '.' || quote_ident(class_sequence.relname)) || - ', COALESCE(MAX(' ||quote_ident(pg_attribute.attname)|| '), 1) ) FROM ' || - quote_ident(table_namespace.nspname)|| '.'||quote_ident(class_table.relname)|| ';' + ', COALESCE(MAX(' || quote_ident(pg_attribute.attname) || '), 1), ' || + 'MAX(' || quote_ident(pg_attribute.attname) || ') IS NOT NULL) FROM ' || + quote_ident(table_namespace.nspname) || '.' || quote_ident(class_table.relname) || ';' FROM pg_depend INNER JOIN pg_class AS class_sequence ON class_sequence.oid = pg_depend.objid diff --git a/scripts/sync-prod.sh b/scripts/sync-prod.sh index e287e19..e77ec4f 100755 --- a/scripts/sync-prod.sh +++ b/scripts/sync-prod.sh @@ -317,13 +317,34 @@ restore_ai_instructions() { || warn "Failed to reload ${AI_INSTRUCTIONS_IMAGES_TABLE} (non-fatal)" fi - PGPASSWORD="$TARGET_DB_PASS" psql \ + ok "AI Compose prompts preserved (no regeneration needed on next boot)" +} + +reset_sequences() { + info "Resetting sequences..." + + local has_function + has_function=$(PGPASSWORD="$TARGET_DB_PASS" psql \ -h "$TARGET_DB_HOST" -p "$TARGET_DB_PORT" -U "$TARGET_DB_USER" \ -d "$TARGET_DB_NAME" \ - -c "SELECT setval(pg_get_serial_sequence('${AI_INSTRUCTIONS_TABLE}', 'id'), GREATEST(COALESCE((SELECT MAX(id) FROM ${AI_INSTRUCTIONS_TABLE}), 1), 1));" >/dev/null 2>&1 \ - || true + -tAc "SELECT to_regclass('maintenance.reset_owned_sequences(text)') IS NOT NULL;" 2>/dev/null) - ok "AI Compose prompts preserved (no regeneration needed on next boot)" + if [[ "$has_function" != "t" ]]; then + warn "maintenance.reset_owned_sequences not available (migrations skipped or not applied); skipping sequence reset" + return 0 + fi + + if ! PGPASSWORD="$TARGET_DB_PASS" psql \ + -h "$TARGET_DB_HOST" \ + -p "$TARGET_DB_PORT" \ + -U "$TARGET_DB_USER" \ + -d "$TARGET_DB_NAME" \ + -v ON_ERROR_STOP=1 \ + -c "SELECT * FROM maintenance.reset_owned_sequences('public');"; then + die "Failed to reset sequences" + fi + + ok "Sequences reset" } sync_database() { @@ -395,15 +416,8 @@ sync_database() { fi fi - info "Resetting sequences..." - PGPASSWORD="$TARGET_DB_PASS" psql \ - -h "$TARGET_DB_HOST" \ - -p "$TARGET_DB_PORT" \ - -U "$TARGET_DB_USER" \ - -d "$TARGET_DB_NAME" \ - -c "SELECT setval(pg_get_serial_sequence(t.table_name, c.column_name), (SELECT COALESCE(MAX(c.column_name::text::bigint), 1) FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = 'id')) FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = 'id' AND c.column_default LIKE 'nextval%' AND t.table_schema = 'public';" 2>/dev/null || true - restore_ai_instructions + reset_sequences ok "Database sync complete: ${TARGET_DB_NAME}" } diff --git a/src/migrations/20261001_000400_add_reset_owned_sequences_function.ts b/src/migrations/20261001_000400_add_reset_owned_sequences_function.ts new file mode 100644 index 0000000..b786bfb --- /dev/null +++ b/src/migrations/20261001_000400_add_reset_owned_sequences_function.ts @@ -0,0 +1,91 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE SCHEMA IF NOT EXISTS maintenance; + + CREATE OR REPLACE FUNCTION maintenance.reset_owned_sequences( + target_schema text DEFAULT 'public' + ) + RETURNS TABLE ( + sequence_name text, + table_name text, + column_name text, + previous_value bigint, + new_value bigint, + is_called boolean + ) + LANGUAGE plpgsql + AS $$ + DECLARE + sequence_record record; + max_value bigint; + prior_value bigint; + should_be_called boolean; + BEGIN + FOR sequence_record IN + SELECT + sequence_namespace.nspname AS sequence_schema, + class_sequence.relname AS sequence_name, + table_namespace.nspname AS table_schema, + class_table.relname AS table_name, + pg_attribute.attname AS column_name + FROM pg_depend + INNER JOIN pg_class AS class_sequence + ON class_sequence.oid = pg_depend.objid + AND class_sequence.relkind = 'S' + INNER JOIN pg_class AS class_table + ON class_table.oid = pg_depend.refobjid + INNER JOIN pg_attribute + ON pg_attribute.attrelid = class_table.oid + AND pg_depend.refobjsubid = pg_attribute.attnum + INNER JOIN pg_namespace AS table_namespace + ON table_namespace.oid = class_table.relnamespace + INNER JOIN pg_namespace AS sequence_namespace + ON sequence_namespace.oid = class_sequence.relnamespace + WHERE table_namespace.nspname = target_schema + AND pg_depend.deptype IN ('a', 'i') + ORDER BY sequence_namespace.nspname, class_sequence.relname + LOOP + EXECUTE format( + 'SELECT COALESCE(MAX(%I), 1), MAX(%I) IS NOT NULL FROM %I.%I', + sequence_record.column_name, + sequence_record.column_name, + sequence_record.table_schema, + sequence_record.table_name + ) + INTO max_value, should_be_called; + + EXECUTE format( + 'SELECT last_value FROM %I.%I', + sequence_record.sequence_schema, + sequence_record.sequence_name + ) + INTO prior_value; + + EXECUTE format( + 'SELECT setval(%L, %s, %L)', + format('%I.%I', sequence_record.sequence_schema, sequence_record.sequence_name), + max_value, + should_be_called + ); + + sequence_name := format('%I.%I', sequence_record.sequence_schema, sequence_record.sequence_name); + table_name := format('%I.%I', sequence_record.table_schema, sequence_record.table_name); + column_name := sequence_record.column_name; + previous_value := prior_value; + new_value := max_value; + is_called := should_be_called; + + RETURN NEXT; + END LOOP; + END; + $$; + `) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + DROP FUNCTION IF EXISTS maintenance.reset_owned_sequences(text); + `) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index e3e1b8d..ba77386 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -97,6 +97,10 @@ import * as migration_20260928_021138_change_ai_model from './20260928_021138_ch import * as migration_20260930_162200_add_documentation_wiki_category from './20260930_162200_add_documentation_wiki_category'; import * as migration_20260930_230747_add_tech_categories from './20260930_230747_add_tech_categories'; import * as migration_20261001_000320_add_tech_tree_position from './20261001_000320_add_tech_tree_position'; +import * as migration_20261001_000400_add_reset_owned_sequences_function from './20261001_000400_add_reset_owned_sequences_function'; +import * as migration_20261002_031917_add_promotion_nominations_and_leadership_transfers from './20261002_031917_add_promotion_nominations_and_leadership_transfers'; +import * as migration_20261002_180000_add_asset_type_accessory from './20261002_180000_add_asset_type_accessory'; +import * as migration_20261003_060154_change_ai_model_to_ornith from './20261003_060154_change_ai_model_to_ornith'; export const migrations = [ { @@ -592,6 +596,26 @@ export const migrations = [ { up: migration_20261001_000320_add_tech_tree_position.up, down: migration_20261001_000320_add_tech_tree_position.down, - name: '20261001_000320_add_tech_tree_position' + name: '20261001_000320_add_tech_tree_position', + }, + { + up: migration_20261001_000400_add_reset_owned_sequences_function.up, + down: migration_20261001_000400_add_reset_owned_sequences_function.down, + name: '20261001_000400_add_reset_owned_sequences_function', + }, + { + up: migration_20261002_031917_add_promotion_nominations_and_leadership_transfers.up, + down: migration_20261002_031917_add_promotion_nominations_and_leadership_transfers.down, + name: '20261002_031917_add_promotion_nominations_and_leadership_transfers' + }, + { + up: migration_20261002_180000_add_asset_type_accessory.up, + down: migration_20261002_180000_add_asset_type_accessory.down, + name: '20261002_180000_add_asset_type_accessory' + }, + { + up: migration_20261003_060154_change_ai_model_to_ornith.up, + down: migration_20261003_060154_change_ai_model_to_ornith.down, + name: '20261003_060154_change_ai_model_to_ornith' }, ]; diff --git a/src/payload-generated-schema.ts b/src/payload-generated-schema.ts index 7b42456..7949562 100644 --- a/src/payload-generated-schema.ts +++ b/src/payload-generated-schema.ts @@ -118,6 +118,8 @@ export const enum_game_event_logs_target_collection = pgEnum( "custom-radio-tests", "custom-radio-test-scores", "ribbon-submissions", + "promotion-nominations", + "leadership-transfers", ], ); export const enum_users_roles = pgEnum("enum_users_roles", [ @@ -575,6 +577,23 @@ export const enum_ribbon_submissions_recipient_scope = pgEnum( "enum_ribbon_submissions_recipient_scope", ["same_command", "whitelist", "anyone"], ); +export const enum_promotion_nominations_status = pgEnum("enum_promotion_nominations_status", [ + "pending", + "awaiting_superuser", + "approved", + "rejected", + "cancelled", +]); +export const enum_promotion_nominations_leader_decision = pgEnum( + "enum_promotion_nominations_leader_decision", + ["pending", "endorsed", "rejected", "not_required"], +); +export const enum_leadership_transfers_status = pgEnum("enum_leadership_transfers_status", [ + "pending", + "approved", + "denied", + "cancelled", +]); export const enum_missions_factions_involved_threat_level = pgEnum( "enum_missions_factions_involved_threat_level", ["ally", "friendly", "neutral", "hostile"], @@ -1031,7 +1050,7 @@ export const enum_plugin_ai_instructions_field_type = pgEnum( ["text", "textarea", "upload", "richText"], ); export const enum_plugin_ai_instructions_model_id = pgEnum("enum_plugin_ai_instructions_model_id", [ - "unsloth/gemma-4-26B-A4B-it-qat-GGUF", + "unsloth/Ornith-1.0-9B-GGUF", ]); export const enum_shims_shims_targeting_roles = pgEnum("enum_shims_shims_targeting_roles", [ "guest", @@ -2741,6 +2760,104 @@ export const ribbon_submissions = pgTable( ], ); +export const promotion_nominations = pgTable( + "promotion_nominations", + { + id: serial("id").primaryKey(), + nominee: integer("nominee_id") + .notNull() + .references(() => users.id, { + onDelete: "set null", + }), + nominator: integer("nominator_id") + .notNull() + .references(() => users.id, { + onDelete: "set null", + }), + accolades: varchar("accolades").notNull(), + status: enum_promotion_nominations_status("status").notNull().default("pending"), + leaderDecision: + enum_promotion_nominations_leader_decision("leader_decision").default("pending"), + leaderDecisionBy: integer("leader_decision_by_id").references(() => users.id, { + onDelete: "set null", + }), + leaderDecidedAt: timestamp("leader_decided_at", { + mode: "string", + withTimezone: true, + precision: 3, + }), + leaderNote: varchar("leader_note"), + reviewNote: varchar("review_note"), + reviewedBy: integer("reviewed_by_id").references(() => users.id, { + onDelete: "set null", + }), + reviewedAt: timestamp("reviewed_at", { mode: "string", withTimezone: true, precision: 3 }), + updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 }) + .defaultNow() + .notNull(), + createdAt: timestamp("created_at", { mode: "string", withTimezone: true, precision: 3 }) + .defaultNow() + .notNull(), + }, + (columns) => [ + index("promotion_nominations_nominee_idx").on(columns.nominee), + index("promotion_nominations_nominator_idx").on(columns.nominator), + index("promotion_nominations_leader_decision_by_idx").on(columns.leaderDecisionBy), + index("promotion_nominations_reviewed_by_idx").on(columns.reviewedBy), + index("promotion_nominations_updated_at_idx").on(columns.updatedAt), + index("promotion_nominations_created_at_idx").on(columns.createdAt), + ], +); + +export const leadership_transfers = pgTable( + "leadership_transfers", + { + id: serial("id").primaryKey(), + initiator: integer("initiator_id") + .notNull() + .references(() => users.id, { + onDelete: "set null", + }), + subject: integer("subject_id") + .notNull() + .references(() => users.id, { + onDelete: "set null", + }), + fromAssignment: integer("from_assignment_id") + .notNull() + .references(() => assignments.id, { + onDelete: "set null", + }), + toAssignment: integer("to_assignment_id") + .notNull() + .references(() => assignments.id, { + onDelete: "set null", + }), + reason: varchar("reason"), + status: enum_leadership_transfers_status("status").notNull().default("pending"), + reviewNote: varchar("review_note"), + reviewedBy: integer("reviewed_by_id").references(() => users.id, { + onDelete: "set null", + }), + resolvedAt: timestamp("resolved_at", { mode: "string", withTimezone: true, precision: 3 }), + updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 }) + .defaultNow() + .notNull(), + createdAt: timestamp("created_at", { mode: "string", withTimezone: true, precision: 3 }) + .defaultNow() + .notNull(), + }, + (columns) => [ + index("leadership_transfers_initiator_idx").on(columns.initiator), + index("leadership_transfers_subject_idx").on(columns.subject), + index("leadership_transfers_from_assignment_idx").on(columns.fromAssignment), + index("leadership_transfers_to_assignment_idx").on(columns.toAssignment), + index("leadership_transfers_reviewed_by_idx").on(columns.reviewedBy), + index("leadership_transfers_updated_at_idx").on(columns.updatedAt), + index("leadership_transfers_created_at_idx").on(columns.createdAt), + ], +); + export const missions_mission_roles_provided_roles = pgTable( "missions_mission_roles_provided_roles", { @@ -6752,8 +6869,8 @@ export const plugin_ai_instructions = pgTable( layout: varchar("layout").default( '[paragraph] - Write a concise introduction (2-3 sentences) that outlines the main topic.\n[horizontalrule] - Insert a horizontal rule to separate the introduction from the main content.\n[list] - Create a list with 3-5 items. Each list item should contain:\n a. [heading] - A brief, descriptive heading (up to 5 words)\n b. [paragraph] - A short explanation or elaboration (1-2 sentences)\n[horizontalrule] - Insert another horizontal rule to separate the main content from the conclusion.\n[paragraph] - Compose a brief conclusion (2-3 sentences) summarizing the key points.\n[quote] - Include a relevant quote from a famous person, directly related to the topic. Format: "Quote text." - Author Name', ), - "gemma-4-settings_model": varchar("gemma_4_settings_model").default( - "unsloth/gemma-4-26B-A4B-it-qat-GGUF", + "ornith-9b-settings_model": varchar("ornith_9b_settings_model").default( + "unsloth/Ornith-1.0-9B-GGUF", ), updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 }) .defaultNow() @@ -6864,6 +6981,14 @@ export const payload_mcp_api_keys = pgTable( ribbonSubmissions_create: boolean("ribbon_submissions_create").default(false), ribbonSubmissions_update: boolean("ribbon_submissions_update").default(false), ribbonSubmissions_delete: boolean("ribbon_submissions_delete").default(false), + promotionNominations_find: boolean("promotion_nominations_find").default(false), + promotionNominations_create: boolean("promotion_nominations_create").default(false), + promotionNominations_update: boolean("promotion_nominations_update").default(false), + promotionNominations_delete: boolean("promotion_nominations_delete").default(false), + leadershipTransfers_find: boolean("leadership_transfers_find").default(false), + leadershipTransfers_create: boolean("leadership_transfers_create").default(false), + leadershipTransfers_update: boolean("leadership_transfers_update").default(false), + leadershipTransfers_delete: boolean("leadership_transfers_delete").default(false), missions_find: boolean("missions_find").default(false), missions_create: boolean("missions_create").default(false), missions_update: boolean("missions_update").default(false), @@ -7158,6 +7283,8 @@ export const payload_locked_documents_rels = pgTable( "user-notificationsID": integer("user_notifications_id"), evaluationsID: integer("evaluations_id"), "ribbon-submissionsID": integer("ribbon_submissions_id"), + "promotion-nominationsID": integer("promotion_nominations_id"), + "leadership-transfersID": integer("leadership_transfers_id"), missionsID: integer("missions_id"), "mission-attendancesID": integer("mission_attendances_id"), "mission-role-signupsID": integer("mission_role_signups_id"), @@ -7253,6 +7380,12 @@ export const payload_locked_documents_rels = pgTable( index("payload_locked_documents_rels_ribbon_submissions_id_idx").on( columns["ribbon-submissionsID"], ), + index("payload_locked_documents_rels_promotion_nominations_id_idx").on( + columns["promotion-nominationsID"], + ), + index("payload_locked_documents_rels_leadership_transfers_id_idx").on( + columns["leadership-transfersID"], + ), index("payload_locked_documents_rels_missions_id_idx").on(columns.missionsID), index("payload_locked_documents_rels_mission_attendances_id_idx").on( columns["mission-attendancesID"], @@ -7462,6 +7595,16 @@ export const payload_locked_documents_rels = pgTable( foreignColumns: [ribbon_submissions.id], name: "payload_locked_documents_rels_ribbon_submissions_fk", }).onDelete("cascade"), + foreignKey({ + columns: [columns["promotion-nominationsID"]], + foreignColumns: [promotion_nominations.id], + name: "payload_locked_documents_rels_promotion_nominations_fk", + }).onDelete("cascade"), + foreignKey({ + columns: [columns["leadership-transfersID"]], + foreignColumns: [leadership_transfers.id], + name: "payload_locked_documents_rels_leadership_transfers_fk", + }).onDelete("cascade"), foreignKey({ columns: [columns["missionsID"]], foreignColumns: [missions.id], @@ -7851,6 +7994,9 @@ export const game_rules = pgTable( currencySingular: varchar("currency_singular"), currencyPlural: varchar("currency_plural"), currencyDenomination: varchar("currency_denomination"), + infantryAssignment: integer("infantry_assignment_id").references(() => assignments.id, { + onDelete: "set null", + }), proximityThreshold: numeric("proximity_threshold", { mode: "number" }).default(1000), gameTickIntervalMinutes: numeric("game_tick_interval_minutes", { mode: "number" }).default(10), lockerGridWidth: numeric("locker_grid_width", { mode: "number" }).default(10), @@ -7883,6 +8029,7 @@ export const game_rules = pgTable( (columns) => [ index("game_rules_main_currency_idx").on(columns.mainCurrency), index("game_rules_currency_symbol_idx").on(columns.currencySymbol), + index("game_rules_infantry_assignment_idx").on(columns.infantryAssignment), ], ); @@ -8640,6 +8787,55 @@ export const relations_ribbon_submissions = relations(ribbon_submissions, ({ one relationName: "grantorTypes", }), })); +export const relations_promotion_nominations = relations(promotion_nominations, ({ one }) => ({ + nominee: one(users, { + fields: [promotion_nominations.nominee], + references: [users.id], + relationName: "nominee", + }), + nominator: one(users, { + fields: [promotion_nominations.nominator], + references: [users.id], + relationName: "nominator", + }), + leaderDecisionBy: one(users, { + fields: [promotion_nominations.leaderDecisionBy], + references: [users.id], + relationName: "leaderDecisionBy", + }), + reviewedBy: one(users, { + fields: [promotion_nominations.reviewedBy], + references: [users.id], + relationName: "reviewedBy", + }), +})); +export const relations_leadership_transfers = relations(leadership_transfers, ({ one }) => ({ + initiator: one(users, { + fields: [leadership_transfers.initiator], + references: [users.id], + relationName: "initiator", + }), + subject: one(users, { + fields: [leadership_transfers.subject], + references: [users.id], + relationName: "subject", + }), + fromAssignment: one(assignments, { + fields: [leadership_transfers.fromAssignment], + references: [assignments.id], + relationName: "fromAssignment", + }), + toAssignment: one(assignments, { + fields: [leadership_transfers.toAssignment], + references: [assignments.id], + relationName: "toAssignment", + }), + reviewedBy: one(users, { + fields: [leadership_transfers.reviewedBy], + references: [users.id], + relationName: "reviewedBy", + }), +})); export const relations_missions_mission_roles_provided_roles = relations( missions_mission_roles_provided_roles, ({ one }) => ({ @@ -10535,6 +10731,16 @@ export const relations_payload_locked_documents_rels = relations( references: [ribbon_submissions.id], relationName: "ribbon-submissions", }), + "promotion-nominationsID": one(promotion_nominations, { + fields: [payload_locked_documents_rels["promotion-nominationsID"]], + references: [promotion_nominations.id], + relationName: "promotion-nominations", + }), + "leadership-transfersID": one(leadership_transfers, { + fields: [payload_locked_documents_rels["leadership-transfersID"]], + references: [leadership_transfers.id], + relationName: "leadership-transfers", + }), missionsID: one(missions, { fields: [payload_locked_documents_rels.missionsID], references: [missions.id], @@ -10889,6 +11095,11 @@ export const relations_game_rules = relations(game_rules, ({ one, many }) => ({ references: [media.id], relationName: "currencySymbol", }), + infantryAssignment: one(assignments, { + fields: [game_rules.infantryAssignment], + references: [assignments.id], + relationName: "infantryAssignment", + }), _rels: many(game_rules_rels, { relationName: "_rels", }), @@ -10974,6 +11185,9 @@ type DatabaseSchema = { enum_ribbon_submissions_grantor_types: typeof enum_ribbon_submissions_grantor_types; enum_ribbon_submissions_status: typeof enum_ribbon_submissions_status; enum_ribbon_submissions_recipient_scope: typeof enum_ribbon_submissions_recipient_scope; + enum_promotion_nominations_status: typeof enum_promotion_nominations_status; + enum_promotion_nominations_leader_decision: typeof enum_promotion_nominations_leader_decision; + enum_leadership_transfers_status: typeof enum_leadership_transfers_status; enum_missions_factions_involved_threat_level: typeof enum_missions_factions_involved_threat_level; enum_missions_logistics_and_support_insertion: typeof enum_missions_logistics_and_support_insertion; enum_missions_logistics_and_support_extraction: typeof enum_missions_logistics_and_support_extraction; @@ -11109,6 +11323,8 @@ type DatabaseSchema = { evaluations: typeof evaluations; ribbon_submissions_grantor_types: typeof ribbon_submissions_grantor_types; ribbon_submissions: typeof ribbon_submissions; + promotion_nominations: typeof promotion_nominations; + leadership_transfers: typeof leadership_transfers; missions_mission_roles_provided_roles: typeof missions_mission_roles_provided_roles; missions_briefing: typeof missions_briefing; missions_mission_objectives: typeof missions_mission_objectives; @@ -11299,6 +11515,8 @@ type DatabaseSchema = { relations_evaluations: typeof relations_evaluations; relations_ribbon_submissions_grantor_types: typeof relations_ribbon_submissions_grantor_types; relations_ribbon_submissions: typeof relations_ribbon_submissions; + relations_promotion_nominations: typeof relations_promotion_nominations; + relations_leadership_transfers: typeof relations_leadership_transfers; relations_missions_mission_roles_provided_roles: typeof relations_missions_mission_roles_provided_roles; relations_missions_briefing: typeof relations_missions_briefing; relations_missions_mission_objectives: typeof relations_missions_mission_objectives; diff --git a/src/payload-types.ts b/src/payload-types.ts index 61adb7e..5881c60 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -89,6 +89,8 @@ export interface Config { 'user-notifications': UserNotification; evaluations: Evaluation; 'ribbon-submissions': RibbonSubmission; + 'promotion-nominations': PromotionNomination; + 'leadership-transfers': LeadershipTransfer; missions: Mission; 'mission-attendances': MissionAttendance; 'mission-role-signups': MissionRoleSignup; @@ -183,6 +185,8 @@ export interface Config { 'user-notifications': UserNotificationsSelect | UserNotificationsSelect; evaluations: EvaluationsSelect | EvaluationsSelect; 'ribbon-submissions': RibbonSubmissionsSelect | RibbonSubmissionsSelect; + 'promotion-nominations': PromotionNominationsSelect | PromotionNominationsSelect; + 'leadership-transfers': LeadershipTransfersSelect | LeadershipTransfersSelect; missions: MissionsSelect | MissionsSelect; 'mission-attendances': MissionAttendancesSelect | MissionAttendancesSelect; 'mission-role-signups': MissionRoleSignupsSelect | MissionRoleSignupsSelect; @@ -1685,6 +1689,7 @@ export interface Asset { | 'optic' | 'muzzle' | 'bipod' + | 'accessory' | 'uniform' | 'vest' | 'backpack' @@ -2386,6 +2391,8 @@ export interface GameEventLog { | 'custom-radio-tests' | 'custom-radio-test-scores' | 'ribbon-submissions' + | 'promotion-nominations' + | 'leadership-transfers' ) | null; targetId?: number | null; @@ -2951,11 +2958,11 @@ export interface Mission { */ 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). + * Server-local day the last web 'not Scheduled' 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). + * Server-local day the last 'not Scheduled' reminder DM was sent to the op creator (managed by the Discord bot, once per day). */ readyReminderDmSentAt?: string | null; /** @@ -2988,6 +2995,9 @@ export interface Mission { ownershipAndStatus: { authors: (number | User)[]; zeus?: (number | User)[] | null; + /** + * Side operations start as Ready (a quiet draft) and are committed to Scheduled by their creator, which posts the Discord roll-call. Only Scheduled/Active missions are driven by the automatic start/complete lifecycle; Concept/Planning/Ready are drafts. + */ status: 'Concept' | 'Planning' | 'Ready' | 'Scheduled' | 'Active' | 'Completed' | 'Cancelled'; visibility: 'unit' | 'leadership' | 'intel' | 'private'; /** @@ -3270,6 +3280,80 @@ export interface RibbonSubmission { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "promotion-nominations". + */ +export interface PromotionNomination { + id: number; + /** + * Member being nominated for promotion. + */ + nominee: number | User; + /** + * Member submitting the nomination (self for self-requests). + */ + nominator: number | User; + /** + * Written examples of performance, accolades, and reasons this member deserves promotion. + */ + accolades: string; + status: 'pending' | 'awaiting_superuser' | 'approved' | 'rejected' | 'cancelled'; + /** + * Decision of the nominee's division leader. + */ + leaderDecision?: ('pending' | 'endorsed' | 'rejected' | 'not_required') | null; + leaderDecisionBy?: (number | null) | User; + leaderDecidedAt?: string | null; + /** + * Note from the division leader's endorsement or rejection. + */ + leaderNote?: string | null; + /** + * Final reviewer feedback shown to the nominator. + */ + reviewNote?: string | null; + reviewedBy?: (number | null) | User; + reviewedAt?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "leadership-transfers". + */ +export interface LeadershipTransfer { + id: number; + /** + * Division leader requesting the removal. + */ + initiator: number | User; + /** + * Member being transferred. + */ + subject: number | User; + /** + * Division the member is removed from. + */ + fromAssignment: number | Assignment; + /** + * Division the member is transferred to (the Infantry assignment). + */ + toAssignment: number | Assignment; + /** + * Optional reason for the removal, visible to command. + */ + reason?: string | null; + status: 'pending' | 'approved' | 'denied' | 'cancelled'; + /** + * Reviewer note recorded with the decision. + */ + reviewNote?: string | null; + reviewedBy?: (number | null) | User; + resolvedAt?: string | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "mission-attendances". @@ -5239,7 +5323,7 @@ export interface PluginAiInstruction { */ 'field-type'?: ('text' | 'textarea' | 'upload' | 'richText') | null; 'relation-to'?: string | null; - 'model-id'?: 'unsloth/gemma-4-26B-A4B-it-qat-GGUF' | null; + 'model-id'?: 'unsloth/Ornith-1.0-9B-GGUF' | null; /** * Please reload your collection after applying the changes */ @@ -5259,7 +5343,7 @@ export interface PluginAiInstruction { | null; system?: string | null; layout?: string | null; - 'gemma-4-settings'?: { + 'ornith-9b-settings'?: { model?: string | null; }; updatedAt: string; @@ -5663,6 +5747,42 @@ export interface PayloadMcpApiKey { */ delete?: boolean | null; }; + promotionNominations?: { + /** + * Allow clients to find promotion-nominations. + */ + find?: boolean | null; + /** + * Allow clients to create promotion-nominations. + */ + create?: boolean | null; + /** + * Allow clients to update promotion-nominations. + */ + update?: boolean | null; + /** + * Allow clients to delete promotion-nominations. + */ + delete?: boolean | null; + }; + leadershipTransfers?: { + /** + * Allow clients to find leadership-transfers. + */ + find?: boolean | null; + /** + * Allow clients to create leadership-transfers. + */ + create?: boolean | null; + /** + * Allow clients to update leadership-transfers. + */ + update?: boolean | null; + /** + * Allow clients to delete leadership-transfers. + */ + delete?: boolean | null; + }; missions?: { /** * Allow clients to find missions. @@ -6768,6 +6888,14 @@ export interface PayloadLockedDocument { relationTo: 'ribbon-submissions'; value: number | RibbonSubmission; } | null) + | ({ + relationTo: 'promotion-nominations'; + value: number | PromotionNomination; + } | null) + | ({ + relationTo: 'leadership-transfers'; + value: number | LeadershipTransfer; + } | null) | ({ relationTo: 'missions'; value: number | Mission; @@ -7696,6 +7824,42 @@ export interface RibbonSubmissionsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "promotion-nominations_select". + */ +export interface PromotionNominationsSelect { + nominee?: T; + nominator?: T; + accolades?: T; + status?: T; + leaderDecision?: T; + leaderDecisionBy?: T; + leaderDecidedAt?: T; + leaderNote?: T; + reviewNote?: T; + reviewedBy?: T; + reviewedAt?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "leadership-transfers_select". + */ +export interface LeadershipTransfersSelect { + initiator?: T; + subject?: T; + fromAssignment?: T; + toAssignment?: T; + reason?: T; + status?: T; + reviewNote?: T; + reviewedBy?: T; + resolvedAt?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "missions_select". @@ -9360,7 +9524,7 @@ export interface PluginAiInstructionsSelect { }; system?: T; layout?: T; - 'gemma-4-settings'?: + 'ornith-9b-settings'?: | T | { model?: T; @@ -9544,6 +9708,22 @@ export interface PayloadMcpApiKeysSelect { update?: T; delete?: T; }; + promotionNominations?: + | T + | { + find?: T; + create?: T; + update?: T; + delete?: T; + }; + leadershipTransfers?: + | T + | { + find?: T; + create?: T; + update?: T; + delete?: T; + }; missions?: | T | { @@ -10068,6 +10248,10 @@ export interface GameRule { * Assignment(s) considered the intelligence division. Members and leaders of these assignments get elevated access to draft missions and intelligence content. */ intelAssignments?: (number | Assignment)[] | null; + /** + * Assignment that members are moved to when a division leader removes them from their division (the default destination for leadership-initiated transfers). + */ + infantryAssignment?: (number | null) | Assignment; /** * Distance threshold in real meters for instant transfers. Structures within this distance can transfer resources instantly. Beyond this, shipments are required. */ @@ -10212,6 +10396,7 @@ export interface GameRulesSelect { currencyDenomination?: T; commandAssignments?: T; intelAssignments?: T; + infantryAssignment?: T; proximityThreshold?: T; gameTickIntervalMinutes?: T; lockerGridWidth?: T;