feat: Wire execution engines for schedules, alerts, wipes, and module install
All checks were successful
Test Asgard Runner / test (push) Successful in 3s

- schedules/: Add schedule executor in SchedulesService — polls every 60s for
  tasks where next_run <= now, dispatches NATS commands per task_type
  (restart, announcement, command, plugin_reload). Calculates next_run from
  cron expression on create/update/toggle. Bootstraps missing next_run values
  on startup. Wire NatsService into SchedulesModule.

- alerts/: Add alert evaluator in AlertsService — polls every 90s, loads all
  alert_config rows, queries latest server_stats per license, evaluates FPS
  degradation and population drop thresholds. Fires alert_history records on
  breach. Enforces 10-minute in-memory cooldown per alert type per license to
  prevent flooding. Wire ServerStats repo into AlertsModule.

- wipes/: Replace hardcoded dry-run mock with profile-aware simulation.
  Resolves actual WipeProfile by ID (cross-tenant protected), builds
  would_delete/would_preserve lists from wipe_type, factors pre_wipe_config
  (backup, countdown warnings) and post_wipe_config (health checks, retry
  attempts) into estimated_duration_seconds. Returns profile_name and notes.

- store/: Fix installModule stub — creates a real module_installations record
  with status='installed' and installed_at timestamp. Idempotent on retry,
  resets failed installations. Wire ModuleInstallation repo into StoreModule.
  getMyModules now returns real installation data instead of filtered purchases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-21 16:02:49 -05:00
parent 9240feedaf
commit cbb3ba6586
7 changed files with 564 additions and 58 deletions

View File

@@ -126,19 +126,112 @@ export class WipesService {
would_delete: string[];
would_preserve: string[];
estimated_duration_seconds: number;
profile_name: string | null;
notes: string[];
}> {
// Stub implementation - real logic would analyze wipe profile config
const mockResult = {
would_delete: ['*.sav', '*.db', 'player.deaths.db', 'player.identities.db'],
would_preserve: ['oxide/', 'oxide/plugins/', 'oxide/data/', 'backups/'],
estimated_duration_seconds: 45,
};
if (dto.wipe_type === 'full') {
mockResult.would_delete.push('oxide/data/*');
mockResult.estimated_duration_seconds = 120;
// Resolve profile config if a profile ID was supplied.
let profile: WipeProfile | null = null;
if (dto.wipe_profile_id) {
profile = await this.wipeProfileRepo.findOne({
where: { id: dto.wipe_profile_id, license_id: licenseId },
});
}
return mockResult;
if (!profile && dto.wipe_profile_id) {
throw new NotFoundException(`Wipe profile ${dto.wipe_profile_id} not found`);
}
const notes: string[] = [];
// Base files affected by all wipe types.
const would_delete: string[] = ['*.map', '*.sav'];
const would_preserve: string[] = [
'oxide/',
'oxide/plugins/',
'cfg/',
'server.cfg',
];
// Blueprint wipe additions.
if (dto.wipe_type === 'blueprint' || dto.wipe_type === 'full') {
would_delete.push('player.blueprints.db', 'player.tech.db');
}
// Full wipe: also clear player data and oxide data.
if (dto.wipe_type === 'full') {
would_delete.push(
'player.deaths.db',
'player.identities.db',
'player.states.db',
'player.tokens.db',
'oxide/data/*',
);
would_preserve.splice(would_preserve.indexOf('oxide/'), 1);
}
// Factor in pre_wipe_config from the profile (if set).
let estimatedSeconds = 45;
if (profile) {
const pre = profile.pre_wipe_config as Record<string, any>;
const post = profile.post_wipe_config as Record<string, any>;
if (pre?.backup_before_wipe) {
estimatedSeconds += 60;
notes.push('Pre-wipe backup will run before deletion (+60s)');
would_preserve.push('backups/');
}
if (pre?.kick_players_before_wipe) {
const countdownWarnings: number[] = (pre.countdown_warnings as number[]) ?? [];
const maxWarning = countdownWarnings.length > 0 ? Math.max(...countdownWarnings) : 0;
if (maxWarning > 0) {
estimatedSeconds += maxWarning * 60;
notes.push(`Players will be warned ${countdownWarnings.join(', ')} minutes before kick (+${maxWarning * 60}s)`);
}
}
if (post?.verify_server_started) {
estimatedSeconds += 30;
notes.push('Post-wipe: server health check will run (+30s)');
}
if (post?.rollback_on_failure) {
notes.push('Rollback on failure is enabled — backup will be preserved if wipe fails');
}
if (post?.max_restart_attempts) {
const attempts = post.max_restart_attempts as number;
if (attempts > 1) {
estimatedSeconds += (attempts - 1) * 15;
notes.push(`Up to ${attempts} restart attempts (+${(attempts - 1) * 15}s max)`);
}
}
} else {
notes.push('No profile selected — using default wipe behavior');
}
// Account for world size in time estimate.
// Larger worlds take longer to clear from disk (rough heuristic).
// We don't have world_size here without querying server_config,
// so apply a static estimate per wipe type.
if (dto.wipe_type === 'full') {
estimatedSeconds += 75;
} else if (dto.wipe_type === 'blueprint') {
estimatedSeconds += 10;
}
this.logger.log(
`Dry-run for license ${licenseId}: type=${dto.wipe_type}, ` +
`profile=${profile?.profile_name ?? 'none'}, estimated=${estimatedSeconds}s`,
);
return {
would_delete,
would_preserve,
estimated_duration_seconds: estimatedSeconds,
profile_name: profile?.profile_name ?? null,
notes,
};
}
}