import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { HttpErrorResponse } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { Api } from '../../core/api'; import { Feedback } from '../../core/feedback'; import { errorMessage, formatDateTime } from '../../core/format'; import type { CaddyInfo, CaddyVersion } from '../../core/models'; import { Icon } from '../../shared/icon'; @Component({ selector: 'app-caddy', imports: [FormsModule, Icon], templateUrl: './caddy.html', styleUrl: './caddy.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class CaddyPage { private api = inject(Api); private feedback = inject(Feedback); protected readonly info = signal(null); protected readonly content = signal(''); protected readonly reason = signal(''); protected readonly saving = signal(false); protected readonly error = signal(null); protected readonly output = signal<{ ok: boolean; text: string } | null>(null); protected readonly versions = signal([]); protected readonly viewed = signal(null); protected readonly dirty = computed( () => this.info() !== null && this.content() !== this.info()!.content, ); protected readonly lineCount = computed(() => this.content().split('\n').length); protected readonly dateTime = formatDateTime; constructor() { this.load(); } protected async load() { try { const [info, versions] = await Promise.all([this.api.caddy(), this.api.caddyVersions()]); this.info.set(info); this.content.set(info.content); this.versions.set(versions); this.error.set(null); } catch (error) { this.error.set(errorMessage(error)); } } protected async save() { if (!this.dirty()) return; const ok = await this.feedback.confirm({ title: 'Enregistrer et recharger Caddy ?', message: 'Si Caddy refuse la nouvelle configuration, l’ancien fichier est remis en place automatiquement.', confirmLabel: 'Enregistrer et recharger', }); if (!ok) return; await this.run( () => this.api.saveCaddy(this.content(), this.reason().trim()), 'Caddyfile enregistré et Caddy rechargé.', ); } protected async reload() { await this.run(() => this.api.reloadCaddy(), 'Caddy rechargé.'); } protected reset() { this.content.set(this.info()?.content ?? ''); this.output.set(null); } protected async view(version: CaddyVersion) { try { this.viewed.set(await this.api.caddyVersion(version.id)); } catch (error) { this.feedback.error(errorMessage(error)); } } protected async restore(version: CaddyVersion) { const ok = await this.feedback.confirm({ title: `Restaurer la version #${version.id} ?`, message: `Version enregistrée le ${formatDateTime(version.createdAt)}. Le fichier actuel est lui-même gardé dans l’historique.`, confirmLabel: 'Restaurer', }); if (!ok) return; this.viewed.set(null); await this.run(() => this.api.restoreCaddy(version.id), `Version #${version.id} restaurée.`); } /** * Tab inserts a tab character (Caddyfile indentation) instead of * leaving the field. */ protected onKeydown(event: KeyboardEvent) { if (event.key !== 'Tab' || event.shiftKey) return; event.preventDefault(); const textarea = event.target as HTMLTextAreaElement; const { selectionStart, selectionEnd, value } = textarea; const next = `${value.slice(0, selectionStart)}\t${value.slice(selectionEnd)}`; this.content.set(next); textarea.value = next; textarea.selectionStart = textarea.selectionEnd = selectionStart + 1; } private async run(action: () => Promise<{ output: string }>, success: string) { this.saving.set(true); this.output.set(null); try { const result = await action(); this.output.set({ ok: true, text: result.output || 'OK' }); this.feedback.success(success); this.reason.set(''); await this.load(); } catch (error) { const body = error instanceof HttpErrorResponse ? error.error : null; this.output.set({ ok: false, text: body?.output || errorMessage(error) }); this.feedback.error(errorMessage(error)); } finally { this.saving.set(false); } } }