feat: Add RaidableBases plugin config module — DB migration, NestJS CRUD, Vue editor
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
- Migration 021: raidablebases_configs table with JSONB config_data - Entity, module, controller (7 endpoints), service with NATS deploy/import - Frontend: 4-tab editor (General, Difficulty, NPC, Loot & Rewards) - Pinia store, types, router route, sidebar nav with Swords icon - Top 30 most common settings with actual RaidableBases.json key paths - Difficulty sub-tabs for Easy/Medium/Hard/Expert/Nightmare with spawn day toggles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import { KitsModule } from './modules/kits/kits.module';
|
|||||||
import { FurnaceSplitterModule } from './modules/furnacesplitter/furnacesplitter.module';
|
import { FurnaceSplitterModule } from './modules/furnacesplitter/furnacesplitter.module';
|
||||||
import { BetterChatModule } from './modules/betterchat/betterchat.module';
|
import { BetterChatModule } from './modules/betterchat/betterchat.module';
|
||||||
import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module';
|
import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module';
|
||||||
|
import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module';
|
||||||
|
|
||||||
// Shared Services
|
// Shared Services
|
||||||
import { NatsService } from './services/nats.service';
|
import { NatsService } from './services/nats.service';
|
||||||
@@ -121,6 +122,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway';
|
|||||||
FurnaceSplitterModule,
|
FurnaceSplitterModule,
|
||||||
BetterChatModule,
|
BetterChatModule,
|
||||||
TimedExecuteModule,
|
TimedExecuteModule,
|
||||||
|
RaidableBasesModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// Global guards (order matters: auth first, then license, then permissions)
|
// Global guards (order matters: auth first, then license, then permissions)
|
||||||
|
|||||||
33
backend-nest/src/entities/raidablebases-config.entity.ts
Normal file
33
backend-nest/src/entities/raidablebases-config.entity.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { License } from './license.entity';
|
||||||
|
|
||||||
|
@Entity('raidablebases_configs')
|
||||||
|
export class RaidableBasesConfig {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
license_id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 100 })
|
||||||
|
config_name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', default: () => "'{}'" })
|
||||||
|
config_data: Record<string, any>;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
is_active: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamptz', default: () => 'NOW()' })
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamptz', default: () => 'NOW()' })
|
||||||
|
updated_at: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => License, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'license_id' })
|
||||||
|
license: License;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { IsString, IsOptional, IsObject, MaxLength } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateRaidableBasesConfigDto {
|
||||||
|
@ApiProperty({ example: 'Default RaidableBases Config' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
config_name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Standard RaidableBases settings' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
config_data?: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { IsString, IsOptional, MaxLength } from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ImportRaidableBasesConfigDto {
|
||||||
|
@ApiProperty({ example: 'Server Import' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
config_name: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Imported from live server' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { IsString, IsOptional, IsObject, IsBoolean, MaxLength } from 'class-validator';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UpdateRaidableBasesConfigDto {
|
||||||
|
@ApiPropertyOptional({ example: 'Updated Config' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
@IsOptional()
|
||||||
|
config_name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 'Updated description' })
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
config_data?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
is_active?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { RaidableBasesService } from './raidablebases.service';
|
||||||
|
import { CreateRaidableBasesConfigDto } from './dto/create-raidablebases-config.dto';
|
||||||
|
import { UpdateRaidableBasesConfigDto } from './dto/update-raidablebases-config.dto';
|
||||||
|
import { ImportRaidableBasesConfigDto } from './dto/import-raidablebases-config.dto';
|
||||||
|
import { CurrentTenant } from '../../common/decorators/current-tenant.decorator';
|
||||||
|
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
|
||||||
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
|
import { PermissionsGuard } from '../../common/guards/permissions.guard';
|
||||||
|
|
||||||
|
@ApiTags('raidablebases')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('raidablebases')
|
||||||
|
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||||
|
export class RaidableBasesController {
|
||||||
|
constructor(private readonly raidableBasesService: RaidableBasesService) {}
|
||||||
|
|
||||||
|
@Get('configs')
|
||||||
|
@RequirePermission('raidablebases.view')
|
||||||
|
@ApiOperation({ summary: 'List RaidableBases configs (summaries)' })
|
||||||
|
getConfigs(@CurrentTenant() licenseId: string) {
|
||||||
|
return this.raidableBasesService.getConfigs(licenseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('configs/:id')
|
||||||
|
@RequirePermission('raidablebases.view')
|
||||||
|
@ApiOperation({ summary: 'Get full RaidableBases config with data' })
|
||||||
|
getConfig(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||||
|
return this.raidableBasesService.getConfig(licenseId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('configs')
|
||||||
|
@RequirePermission('raidablebases.manage')
|
||||||
|
@ApiOperation({ summary: 'Create RaidableBases config' })
|
||||||
|
createConfig(@CurrentTenant() licenseId: string, @Body() dto: CreateRaidableBasesConfigDto) {
|
||||||
|
return this.raidableBasesService.createConfig(licenseId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('configs/:id')
|
||||||
|
@RequirePermission('raidablebases.manage')
|
||||||
|
@ApiOperation({ summary: 'Update RaidableBases config' })
|
||||||
|
updateConfig(
|
||||||
|
@CurrentTenant() licenseId: string,
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: UpdateRaidableBasesConfigDto,
|
||||||
|
) {
|
||||||
|
return this.raidableBasesService.updateConfig(licenseId, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('configs/:id')
|
||||||
|
@RequirePermission('raidablebases.manage')
|
||||||
|
@ApiOperation({ summary: 'Delete RaidableBases config' })
|
||||||
|
deleteConfig(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||||
|
return this.raidableBasesService.deleteConfig(licenseId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('configs/:id/apply')
|
||||||
|
@RequirePermission('raidablebases.manage')
|
||||||
|
@ApiOperation({ summary: 'Deploy RaidableBases config to server' })
|
||||||
|
applyToServer(@CurrentTenant() licenseId: string, @Param('id') id: string) {
|
||||||
|
return this.raidableBasesService.applyToServer(licenseId, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('import-from-server')
|
||||||
|
@RequirePermission('raidablebases.manage')
|
||||||
|
@ApiOperation({ summary: 'Import RaidableBases.json from server via NATS' })
|
||||||
|
importFromServer(@CurrentTenant() licenseId: string, @Body() dto: ImportRaidableBasesConfigDto) {
|
||||||
|
return this.raidableBasesService.importFromServer(licenseId, dto.config_name, dto.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { RaidableBasesController } from './raidablebases.controller';
|
||||||
|
import { RaidableBasesService } from './raidablebases.service';
|
||||||
|
import { RaidableBasesConfig } from '../../entities/raidablebases-config.entity';
|
||||||
|
import { NatsService } from '../../services/nats.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([RaidableBasesConfig])],
|
||||||
|
controllers: [RaidableBasesController],
|
||||||
|
providers: [RaidableBasesService, NatsService],
|
||||||
|
exports: [RaidableBasesService],
|
||||||
|
})
|
||||||
|
export class RaidableBasesModule {}
|
||||||
180
backend-nest/src/modules/raidablebases/raidablebases.service.ts
Normal file
180
backend-nest/src/modules/raidablebases/raidablebases.service.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { Injectable, Logger, NotFoundException, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { RaidableBasesConfig } from '../../entities/raidablebases-config.entity';
|
||||||
|
import { NatsService } from '../../services/nats.service';
|
||||||
|
import { CreateRaidableBasesConfigDto } from './dto/create-raidablebases-config.dto';
|
||||||
|
import { UpdateRaidableBasesConfigDto } from './dto/update-raidablebases-config.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RaidableBasesService {
|
||||||
|
private readonly logger = new Logger(RaidableBasesService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(RaidableBasesConfig)
|
||||||
|
private readonly raidableBasesRepo: Repository<RaidableBasesConfig>,
|
||||||
|
private readonly natsService: NatsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** List configs for a license (summaries — no JSONB) */
|
||||||
|
async getConfigs(licenseId: string) {
|
||||||
|
const configs = await this.raidableBasesRepo.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.raidableBasesRepo.findOne({
|
||||||
|
where: { id: configId, license_id: licenseId },
|
||||||
|
});
|
||||||
|
if (!config) throw new NotFoundException('RaidableBases config not found');
|
||||||
|
return { config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a new config */
|
||||||
|
async createConfig(licenseId: string, dto: CreateRaidableBasesConfigDto) {
|
||||||
|
const config = this.raidableBasesRepo.create({
|
||||||
|
license_id: licenseId,
|
||||||
|
config_name: dto.config_name,
|
||||||
|
description: dto.description || null,
|
||||||
|
config_data: dto.config_data || {},
|
||||||
|
});
|
||||||
|
const saved = await this.raidableBasesRepo.save(config);
|
||||||
|
return { config: saved };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update an existing config */
|
||||||
|
async updateConfig(licenseId: string, configId: string, dto: UpdateRaidableBasesConfigDto) {
|
||||||
|
const config = await this.raidableBasesRepo.findOne({
|
||||||
|
where: { id: configId, license_id: licenseId },
|
||||||
|
});
|
||||||
|
if (!config) throw new NotFoundException('RaidableBases 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.raidableBasesRepo.save(config);
|
||||||
|
return { config: saved };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete a config */
|
||||||
|
async deleteConfig(licenseId: string, configId: string) {
|
||||||
|
const result = await this.raidableBasesRepo.delete({ id: configId, license_id: licenseId });
|
||||||
|
if (result.affected === 0) throw new NotFoundException('RaidableBases config not found');
|
||||||
|
return { deleted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deploy config to game server via NATS */
|
||||||
|
async applyToServer(licenseId: string, configId: string) {
|
||||||
|
const config = await this.raidableBasesRepo.findOne({
|
||||||
|
where: { id: configId, license_id: licenseId },
|
||||||
|
});
|
||||||
|
if (!config) throw new NotFoundException('RaidableBases config not found');
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(config.config_data, null, 2);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write RaidableBases.json via file manager NATS
|
||||||
|
await this.natsService.request(
|
||||||
|
`corrosion.${licenseId}.files.cmd`,
|
||||||
|
{
|
||||||
|
func: 'fm_save',
|
||||||
|
path: 'server://oxide/config/RaidableBases.json',
|
||||||
|
content: jsonString,
|
||||||
|
},
|
||||||
|
30000,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reload RaidableBases plugin via RCON
|
||||||
|
await this.natsService.publish(
|
||||||
|
`corrosion.${licenseId}.cmd.server`,
|
||||||
|
{
|
||||||
|
action: 'command',
|
||||||
|
command: 'oxide.reload RaidableBases',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mark this config as active, deactivate others
|
||||||
|
await this.raidableBasesRepo.update({ license_id: licenseId }, { is_active: false });
|
||||||
|
await this.raidableBasesRepo.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 RaidableBases config: ${(error as Error).message}`);
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to deploy RaidableBases config — agent may be offline',
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import RaidableBases.json from game server via NATS */
|
||||||
|
async importFromServer(licenseId: string, configName: string, description?: string) {
|
||||||
|
try {
|
||||||
|
// Read RaidableBases.json from server via file manager NATS
|
||||||
|
const response = await this.natsService.request(
|
||||||
|
`corrosion.${licenseId}.files.cmd`,
|
||||||
|
{
|
||||||
|
func: 'fm_preview',
|
||||||
|
path: 'server://oxide/config/RaidableBases.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 RaidableBases config row
|
||||||
|
const config = this.raidableBasesRepo.create({
|
||||||
|
license_id: licenseId,
|
||||||
|
config_name: configName,
|
||||||
|
description: description || 'Imported from server',
|
||||||
|
config_data: configData,
|
||||||
|
});
|
||||||
|
const saved = await this.raidableBasesRepo.save(config);
|
||||||
|
|
||||||
|
return { config: saved };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(`Failed to import RaidableBases config from server: ${(error as Error).message}`);
|
||||||
|
throw new HttpException(
|
||||||
|
'Failed to import RaidableBases config — agent may be offline',
|
||||||
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
backend/migrations/021_raidablebases_configs.sql
Normal file
11
backend/migrations/021_raidablebases_configs.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS raidablebases_configs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
license_id UUID NOT NULL REFERENCES licenses(id) ON DELETE CASCADE,
|
||||||
|
config_name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
config_data JSONB NOT NULL DEFAULT '{}',
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_raidablebases_configs_license ON raidablebases_configs(license_id);
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
DoorOpen,
|
DoorOpen,
|
||||||
Gift,
|
Gift,
|
||||||
Flame,
|
Flame,
|
||||||
|
Swords,
|
||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
@@ -58,6 +59,7 @@ const navItems = [
|
|||||||
{ name: 'Furnace Splitter', path: '/furnace-splitter', icon: Flame, permission: 'furnacesplitter.view' },
|
{ name: 'Furnace Splitter', path: '/furnace-splitter', icon: Flame, permission: 'furnacesplitter.view' },
|
||||||
{ name: 'Better Chat', path: '/better-chat', icon: MessageSquare, permission: 'betterchat.view' },
|
{ name: 'Better Chat', path: '/better-chat', icon: MessageSquare, permission: 'betterchat.view' },
|
||||||
{ name: 'Timed Execute', path: '/timed-execute', icon: Clock, permission: 'timedexecute.view' },
|
{ name: 'Timed Execute', path: '/timed-execute', icon: Clock, permission: 'timedexecute.view' },
|
||||||
|
{ name: 'Raidable Bases', path: '/raidable-bases', icon: Swords, permission: 'raidablebases.view' },
|
||||||
{ name: 'Auto-Wiper', path: '/wipes', icon: RefreshCw, permission: 'wipes.view' },
|
{ name: 'Auto-Wiper', path: '/wipes', icon: RefreshCw, permission: 'wipes.view' },
|
||||||
{ name: 'Maps', path: '/maps', icon: Map, permission: 'maps.view' },
|
{ name: 'Maps', path: '/maps', icon: Map, permission: 'maps.view' },
|
||||||
{ name: 'Chat Log', path: '/chat', icon: MessageSquare, permission: 'chat.view' },
|
{ name: 'Chat Log', path: '/chat', icon: MessageSquare, permission: 'chat.view' },
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ const panelRoutes: RouteRecordRaw[] = [
|
|||||||
name: 'timed-execute',
|
name: 'timed-execute',
|
||||||
component: () => import('@/views/admin/TimedExecuteView.vue'),
|
component: () => import('@/views/admin/TimedExecuteView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'raidable-bases',
|
||||||
|
name: 'raidable-bases',
|
||||||
|
component: () => import('@/views/admin/RaidableBasesView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'wipes',
|
path: 'wipes',
|
||||||
name: 'wipes',
|
name: 'wipes',
|
||||||
|
|||||||
145
frontend/src/stores/raidablebases.ts
Normal file
145
frontend/src/stores/raidablebases.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useApi } from '@/composables/useApi'
|
||||||
|
import { useToastStore } from '@/stores/toast'
|
||||||
|
import type { RaidableBasesConfigSummary, RaidableBasesConfigFull, RaidableBasesApplyResult } from '@/types'
|
||||||
|
|
||||||
|
export const useRaidableBasesStore = defineStore('raidablebases', () => {
|
||||||
|
const configs = ref<RaidableBasesConfigSummary[]>([])
|
||||||
|
const currentConfig = ref<RaidableBasesConfigFull | null>(null)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const isApplying = ref(false)
|
||||||
|
const isDirty = ref(false)
|
||||||
|
const api = useApi()
|
||||||
|
const toast = useToastStore()
|
||||||
|
|
||||||
|
async function fetchConfigs() {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.get<{ configs: RaidableBasesConfigSummary[] }>('/raidablebases/configs')
|
||||||
|
configs.value = res.configs
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig(id: string) {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.get<{ config: RaidableBasesConfigFull }>(`/raidablebases/configs/${id}`)
|
||||||
|
currentConfig.value = res.config
|
||||||
|
isDirty.value = false
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createConfig(name: string, description?: string) {
|
||||||
|
try {
|
||||||
|
const res = await api.post<{ config: RaidableBasesConfigFull }>('/raidablebases/configs', {
|
||||||
|
config_name: name,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
await fetchConfigs()
|
||||||
|
currentConfig.value = res.config
|
||||||
|
isDirty.value = false
|
||||||
|
toast.success(`Config "${name}" created`)
|
||||||
|
return res.config
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCurrentConfig() {
|
||||||
|
if (!currentConfig.value) return
|
||||||
|
isSaving.value = true
|
||||||
|
try {
|
||||||
|
await api.put(`/raidablebases/configs/${currentConfig.value.id}`, {
|
||||||
|
config_name: currentConfig.value.config_name,
|
||||||
|
description: currentConfig.value.description,
|
||||||
|
config_data: currentConfig.value.config_data,
|
||||||
|
})
|
||||||
|
isDirty.value = false
|
||||||
|
await fetchConfigs()
|
||||||
|
toast.success('Config saved')
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteConfig(id: string) {
|
||||||
|
try {
|
||||||
|
await api.del(`/raidablebases/configs/${id}`)
|
||||||
|
if (currentConfig.value?.id === id) {
|
||||||
|
currentConfig.value = null
|
||||||
|
}
|
||||||
|
await fetchConfigs()
|
||||||
|
toast.success('Config deleted')
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyToServer(id: string) {
|
||||||
|
isApplying.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.post<RaidableBasesApplyResult>(`/raidablebases/configs/${id}/apply`)
|
||||||
|
await fetchConfigs()
|
||||||
|
toast.success(res.message)
|
||||||
|
return res
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
isApplying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importFromServer(configName: string) {
|
||||||
|
isLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.post<{ config: RaidableBasesConfigFull }>('/raidablebases/import-from-server', {
|
||||||
|
config_name: configName,
|
||||||
|
})
|
||||||
|
await fetchConfigs()
|
||||||
|
currentConfig.value = res.config
|
||||||
|
isDirty.value = false
|
||||||
|
toast.success(`Config imported from server as "${configName}"`)
|
||||||
|
return res.config
|
||||||
|
} catch (err) {
|
||||||
|
toast.error((err as Error).message)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markDirty() {
|
||||||
|
isDirty.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configs,
|
||||||
|
currentConfig,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
isApplying,
|
||||||
|
isDirty,
|
||||||
|
fetchConfigs,
|
||||||
|
loadConfig,
|
||||||
|
createConfig,
|
||||||
|
saveCurrentConfig,
|
||||||
|
deleteConfig,
|
||||||
|
applyToServer,
|
||||||
|
importFromServer,
|
||||||
|
markDirty,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -708,3 +708,30 @@ export interface TimedExecuteApplyResult {
|
|||||||
message: string
|
message: string
|
||||||
config_name: string
|
config_name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RaidableBases Config types
|
||||||
|
export interface RaidableBasesConfigSummary {
|
||||||
|
id: string
|
||||||
|
config_name: string
|
||||||
|
description: string | null
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RaidableBasesConfigFull {
|
||||||
|
id: string
|
||||||
|
license_id: string
|
||||||
|
config_name: string
|
||||||
|
description: string | null
|
||||||
|
config_data: Record<string, any>
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RaidableBasesApplyResult {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
config_name: string
|
||||||
|
}
|
||||||
|
|||||||
1143
frontend/src/views/admin/RaidableBasesView.vue
Normal file
1143
frontend/src/views/admin/RaidableBasesView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user