All checks were successful
Test Asgard Runner / test (push) Successful in 2s
- DB migrations 017 (betterchat_configs) and 020 (timedexecute_configs) applied - TypeORM entities matching production schema exactly - NestJS modules with full CRUD + apply-to-server + import-from-server - Pinia stores following teleport config pattern - BetterChatView: Chat Groups editor with color pickers, font sizes, format strings; Settings tab with word filter, anti-flood, player tagging - TimedExecuteView: TimerRepeat with presets, RealTime-Timer, OnConnect/OnDisconnect command lists - Wired into app.module.ts, router, DashboardLayout nav Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
181 lines
6.2 KiB
TypeScript
181 lines
6.2 KiB
TypeScript
import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { TimedExecuteConfig } from '../../entities/timedexecute-config.entity';
|
|
import { NatsService } from '../../services/nats.service';
|
|
import { CreateTimedExecuteConfigDto } from './dto/create-timedexecute-config.dto';
|
|
import { UpdateTimedExecuteConfigDto } from './dto/update-timedexecute-config.dto';
|
|
|
|
@Injectable()
|
|
export class TimedExecuteService {
|
|
private readonly logger = new Logger(TimedExecuteService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(TimedExecuteConfig)
|
|
private readonly repo: Repository<TimedExecuteConfig>,
|
|
private readonly natsService: NatsService,
|
|
) {}
|
|
|
|
/** List configs for a license (summaries — no JSONB) */
|
|
async getConfigs(licenseId: string) {
|
|
const configs = await this.repo.find({
|
|
where: { license_id: licenseId },
|
|
select: ['id', 'config_name', 'description', 'is_active', 'created_at', 'updated_at'],
|
|
order: { created_at: 'DESC' },
|
|
});
|
|
return { configs };
|
|
}
|
|
|
|
/** Get full config with JSONB data */
|
|
async getConfig(licenseId: string, configId: string) {
|
|
const config = await this.repo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('TimedExecute config not found');
|
|
return { config };
|
|
}
|
|
|
|
/** Create a new config */
|
|
async createConfig(licenseId: string, dto: CreateTimedExecuteConfigDto) {
|
|
const config = this.repo.create({
|
|
license_id: licenseId,
|
|
config_name: dto.config_name,
|
|
description: dto.description || null,
|
|
config_data: dto.config_data || {},
|
|
});
|
|
const saved = await this.repo.save(config);
|
|
return { config: saved };
|
|
}
|
|
|
|
/** Update an existing config */
|
|
async updateConfig(licenseId: string, configId: string, dto: UpdateTimedExecuteConfigDto) {
|
|
const config = await this.repo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('TimedExecute config not found');
|
|
|
|
if (dto.config_name !== undefined) config.config_name = dto.config_name;
|
|
if (dto.description !== undefined) config.description = dto.description;
|
|
if (dto.config_data !== undefined) config.config_data = dto.config_data;
|
|
if (dto.is_active !== undefined) config.is_active = dto.is_active;
|
|
config.updated_at = new Date();
|
|
|
|
const saved = await this.repo.save(config);
|
|
return { config: saved };
|
|
}
|
|
|
|
/** Delete a config */
|
|
async deleteConfig(licenseId: string, configId: string) {
|
|
const result = await this.repo.delete({ id: configId, license_id: licenseId });
|
|
if (result.affected === 0) throw new NotFoundException('TimedExecute config not found');
|
|
return { deleted: true };
|
|
}
|
|
|
|
/** Deploy config to game server via NATS */
|
|
async applyToServer(licenseId: string, configId: string) {
|
|
const config = await this.repo.findOne({
|
|
where: { id: configId, license_id: licenseId },
|
|
});
|
|
if (!config) throw new NotFoundException('TimedExecute config not found');
|
|
|
|
const jsonString = JSON.stringify(config.config_data, null, 2);
|
|
|
|
try {
|
|
// Write TimedExecute.json via file manager NATS
|
|
await this.natsService.request(
|
|
`corrosion.${licenseId}.files.cmd`,
|
|
{
|
|
func: 'fm_save',
|
|
path: 'server://oxide/config/TimedExecute.json',
|
|
content: jsonString,
|
|
},
|
|
30000,
|
|
);
|
|
|
|
// Reload TimedExecute plugin via RCON
|
|
await this.natsService.publish(
|
|
`corrosion.${licenseId}.cmd.server`,
|
|
{
|
|
action: 'command',
|
|
command: 'oxide.reload TimedExecute',
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
);
|
|
|
|
// Mark this config as active, deactivate others
|
|
await this.repo.update({ license_id: licenseId }, { is_active: false });
|
|
await this.repo.update(
|
|
{ id: configId, license_id: licenseId },
|
|
{ is_active: true, updated_at: new Date() },
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
message: `Config "${config.config_name}" deployed to server`,
|
|
config_name: config.config_name,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Failed to deploy TimedExecute config: ${(error as Error).message}`);
|
|
throw new HttpException(
|
|
'Failed to deploy TimedExecute config — agent may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Import TimedExecute.json from game server via NATS */
|
|
async importFromServer(licenseId: string, configName: string, description?: string) {
|
|
try {
|
|
// Read TimedExecute.json from server via file manager NATS
|
|
const response = await this.natsService.request(
|
|
`corrosion.${licenseId}.files.cmd`,
|
|
{
|
|
func: 'fm_preview',
|
|
path: 'server://oxide/config/TimedExecute.json',
|
|
},
|
|
30000,
|
|
);
|
|
|
|
if (!response) {
|
|
throw new HttpException(
|
|
'No response from agent — it may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
|
|
// Parse the response content as JSON
|
|
const responseData = response as Record<string, any>;
|
|
let configData: Record<string, any>;
|
|
|
|
if (typeof responseData.content === 'string') {
|
|
configData = JSON.parse(responseData.content);
|
|
} else if (typeof responseData.content === 'object') {
|
|
configData = responseData.content;
|
|
} else {
|
|
throw new HttpException(
|
|
'Unexpected response format from agent',
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Create new config row
|
|
const config = this.repo.create({
|
|
license_id: licenseId,
|
|
config_name: configName,
|
|
description: description || 'Imported from server',
|
|
config_data: configData,
|
|
});
|
|
const saved = await this.repo.save(config);
|
|
|
|
return { config: saved };
|
|
} catch (error) {
|
|
if (error instanceof HttpException) throw error;
|
|
this.logger.error(`Failed to import TimedExecute config from server: ${(error as Error).message}`);
|
|
throw new HttpException(
|
|
'Failed to import TimedExecute config — agent may be offline',
|
|
HttpStatus.SERVICE_UNAVAILABLE,
|
|
);
|
|
}
|
|
}
|
|
}
|