first commit

This commit is contained in:
Rutra committed 2026-10-12 14:25:22 +02:00
commit 3c0479faf0
169 files changed
+27617

No files matched your search

+177
View File
@@ -0,0 +1,177 @@
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { Api } from '../../core/api';
import { errorMessage } from '../../core/format';
import type {
AuditStatus,
CreateProjectInput,
ProjectOptions,
ProjectType,
} from '../../core/models';
import { Icon } from '../../shared/icon';
import { JobConsole } from '../../shared/job-console';
const TYPES: { value: ProjectType; label: string; description: string }[] = [
{
value: 'node',
label: 'Node.js',
description: 'Une app Node lancée par une commande (node start.js…).',
},
{
value: 'adonis',
label: 'AdonisJS',
description: 'Compilée (node ace build) puis lancée en production.',
},
{ value: 'wordpress', label: 'WordPress', description: 'Image officielle, base MariaDB dédiée.' },
{
value: 'static',
label: 'Site statique',
description: 'Fichiers servis directement par Caddy.',
},
];
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,40}$/;
@Component({
selector: 'app-project-new',
imports: [FormsModule, RouterLink, Icon, JobConsole],
templateUrl: './project-new.html',
styleUrl: './project-new.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProjectNewPage {
private api = inject(Api);
protected readonly types = TYPES;
protected readonly options = signal<ProjectOptions | null>(null);
protected readonly type = signal<ProjectType>('node');
protected readonly name = signal('');
protected readonly category = signal('api');
protected readonly gitUrl = signal('');
protected readonly port = signal(3333);
protected readonly command = signal('');
protected readonly env = signal('');
protected readonly createDatabase = signal(false);
protected readonly addCaddy = signal(true);
protected readonly spa = signal(true);
/**
* Null while the domain follows the name.
*/
protected readonly customDomain = signal<string | null>(null);
protected readonly submitting = signal(false);
protected readonly error = signal<string | null>(null);
protected readonly jobId = signal<number | null>(null);
protected readonly jobStatus = signal<AuditStatus | null>(null);
protected readonly domain = computed(
() =>
this.customDomain() ??
(this.name() ? `${this.name()}.${this.options()?.baseDomain ?? ''}` : ''),
);
protected readonly isApp = computed(() => this.type() === 'node' || this.type() === 'adonis');
protected readonly nameValid = computed(() => NAME_PATTERN.test(this.name()));
protected readonly targetPath = computed(() => {
const root = this.options()?.srvRoot ?? '/srv';
const folder =
this.type() === 'static'
? 'html'
: this.type() === 'wordpress'
? 'wordpress'
: this.category();
return `${root}/${folder}/${this.name() || '<nom>'}`.replace(/\\/g, '/');
});
/**
* What the creation will do, step by step.
*/
protected readonly steps = computed(() => {
const steps: string[] = [`Créer le dossier ${this.targetPath()}`];
const type = this.type();
if (type === 'static') {
steps.push('Y mettre une page d’attente (à remplacer par ton build)');
} else {
if (this.isApp()) steps.push('Cloner le dépôt dans project/');
if (type === 'wordpress' || this.createDatabase()) {
steps.push(
`Créer la base MariaDB « ${(this.name() || '<nom>').replace(/-/g, '_')} » et son utilisateur`,
);
}
steps.push(
type === 'wordpress' ? 'Écrire le .env (identifiants de la base)' : 'Écrire project/.env',
);
steps.push('Écrire le docker-compose.yml (template de la procédure)');
steps.push('Lancer : docker compose up -d');
}
if (this.addCaddy())
steps.push(`Ajouter ${this.domain() || '<domaine>'} au Caddyfile et recharger Caddy`);
return steps;
});
constructor() {
this.api
.projectOptions()
.then((options) => {
this.options.set(options);
this.command.set(options.defaultNodeCommand);
})
.catch((error) => this.error.set(errorMessage(error)));
}
protected selectType(type: ProjectType) {
this.type.set(type);
if (type === 'wordpress') this.category.set('wordpress');
else if (this.category() === 'wordpress') this.category.set('api');
}
protected async submit() {
this.error.set(null);
if (!this.nameValid()) {
this.error.set('Le nom ne peut contenir que des minuscules, des chiffres et des tirets.');
return;
}
if (this.isApp() && !this.gitUrl().trim()) {
this.error.set('L’URL du dépôt git est obligatoire.');
return;
}
const type = this.type();
const input: CreateProjectInput = {
type,
name: this.name(),
category: type === 'wordpress' ? 'wordpress' : this.category(),
createDatabase: type === 'wordpress' || (this.isApp() && this.createDatabase()),
domain: this.domain(),
addCaddy: this.addCaddy(),
spa: this.spa(),
};
if (this.isApp()) {
input.gitUrl = this.gitUrl().trim();
input.port = Number(this.port());
input.env = this.env();
if (type === 'node') input.command = this.command().trim();
}
this.submitting.set(true);
try {
const { jobId } = await this.api.createProject(input);
this.jobStatus.set(null);
this.jobId.set(jobId);
} catch (error) {
this.error.set(errorMessage(error));
} finally {
this.submitting.set(false);
}
}
protected projectLink() {
const type = this.type();
const category =
type === 'static' ? 'html' : type === 'wordpress' ? 'wordpress' : this.category();
return ['/projects', category, this.name()];
}
}