feat: Waves 3+4 — frontend wiring, NATS integration, stores (19 files)
All checks were successful
Test Asgard Runner / test (push) Successful in 2s

Frontend:
- Wire Dashboard quick actions (start/stop/trigger wipe) + next wipe schedule
- Wire Console WebSocket streaming for real-time output
- Implement TOTP 2FA challenge flow in LoginView
- Wire Plugin load/unload toggle + uninstall buttons with confirmations
- Wire WipesView profile selector, disable trigger when no profiles
- Build full WipeProfiles create/edit modal with all config fields
- Wire MapsView file upload with multipart FormData
- Fix SettingsView empty catch blocks → toast error messages
- Fix stale localStorage token reads in CSV exports → auth store
- Fix auth store hardcoded permissions → JWT-decoded role permissions
- Fix wipe store onMounted lifecycle bug → explicit subscribe action
- Update EarlyAccessView from countdown to "Now Live" state

Backend:
- Wire wipe trigger to publish NATS cmd (corrosion.{id}.cmd.wipe)
- Wire plugin reload/uninstall to publish NATS cmd
- Expand NatsBridgeService: add files, wipe status, server status subs
- Add PATCH schedules/:id/toggle endpoint for task toggling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-21 13:34:09 -05:00
parent a181ed7ded
commit 8bb6cc0890
19 changed files with 776 additions and 139 deletions

View File

@@ -3,11 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { PluginsController } from './plugins.controller';
import { PluginsService } from './plugins.service';
import { PluginRegistry } from '../../entities/plugin-registry.entity';
import { NatsService } from '../../services/nats.service';
@Module({
imports: [TypeOrmModule.forFeature([PluginRegistry])],
controllers: [PluginsController],
providers: [PluginsService],
providers: [PluginsService, NatsService],
exports: [PluginsService],
})
export class PluginsModule {}

View File

@@ -3,9 +3,11 @@ import {
Get,
Post,
Put,
Patch,
Delete,
Body,
Param,
ParseUUIDPipe,
UseGuards,
} from '@nestjs/common';
import {
@@ -85,6 +87,28 @@ export class SchedulesController {
return await this.schedulesService.updateTask(licenseId, taskId, dto);
}
@Patch('tasks/:id/toggle')
@RequirePermission('schedules.manage')
@ApiOperation({
summary: 'Toggle a scheduled task',
description: 'Enable or disable a scheduled task without deleting it',
})
@ApiResponse({
status: 200,
description: 'Task toggled successfully',
})
@ApiResponse({
status: 404,
description: 'Task not found',
})
async toggleTask(
@CurrentTenant() licenseId: string,
@Param('id', ParseUUIDPipe) taskId: string,
@Body('enabled') enabled: boolean,
) {
return await this.schedulesService.toggleTask(licenseId, taskId, enabled);
}
@Delete('tasks/:id')
@RequirePermission('schedules.manage')
@ApiOperation({
@@ -101,7 +125,7 @@ export class SchedulesController {
})
async deleteTask(
@CurrentTenant() licenseId: string,
@Param('id') taskId: string,
@Param('id', ParseUUIDPipe) taskId: string,
) {
return await this.schedulesService.deleteTask(licenseId, taskId);
}

View File

@@ -5,11 +5,12 @@ import { WipesService } from './wipes.service';
import { WipeProfile } from '../../entities/wipe-profile.entity';
import { WipeSchedule } from '../../entities/wipe-schedule.entity';
import { WipeHistory } from '../../entities/wipe-history.entity';
import { NatsService } from '../../services/nats.service';
@Module({
imports: [TypeOrmModule.forFeature([WipeProfile, WipeSchedule, WipeHistory])],
controllers: [WipesController],
providers: [WipesService],
providers: [WipesService, NatsService],
exports: [WipesService],
})
export class WipesModule {}

View File

@@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WipeProfile } from '../../entities/wipe-profile.entity';
@@ -8,9 +8,12 @@ import { CreateProfileDto } from './dto/create-profile.dto';
import { UpdateProfileDto } from './dto/update-profile.dto';
import { CreateScheduleDto } from './dto/create-schedule.dto';
import { TriggerWipeDto } from './dto/trigger-wipe.dto';
import { NatsService } from '../../services/nats.service';
@Injectable()
export class WipesService {
private readonly logger = new Logger(WipesService.name);
constructor(
@InjectRepository(WipeProfile)
private readonly wipeProfileRepo: Repository<WipeProfile>,
@@ -18,6 +21,7 @@ export class WipesService {
private readonly wipeScheduleRepo: Repository<WipeSchedule>,
@InjectRepository(WipeHistory)
private readonly wipeHistoryRepo: Repository<WipeHistory>,
private readonly natsService: NatsService,
) {}
async getProfiles(licenseId: string): Promise<WipeProfile[]> {
@@ -102,6 +106,16 @@ export class WipesService {
});
const saved = await this.wipeHistoryRepo.save(history);
await this.natsService.publish(`corrosion.${licenseId}.cmd.wipe`, {
wipe_history_id: saved.id,
wipe_type: dto.wipe_type,
wipe_profile_id: dto.wipe_profile_id ?? null,
trigger_type: 'manual',
timestamp: new Date().toISOString(),
});
this.logger.log(`Wipe triggered for license ${licenseId} — history id ${saved.id}`);
return { wipe_history_id: saved.id };
}

View File

@@ -19,6 +19,21 @@ export class NatsBridgeService implements OnModuleInit {
this.emit(licenseId, 'console_output', data);
});
this.nats.subscribe('corrosion.*.files.response', (data, subject) => {
const licenseId = subject.split('.')[1];
this.emit(licenseId, 'files_response', data);
});
this.nats.subscribe('corrosion.*.wipe.status', (data, subject) => {
const licenseId = subject.split('.')[1];
this.emit(licenseId, 'wipe_status', data);
});
this.nats.subscribe('corrosion.*.server.status', (data, subject) => {
const licenseId = subject.split('.')[1];
this.emit(licenseId, 'server_status', data);
});
this.logger.log('NATS bridge subscriptions initialized');
}