final stage

This commit is contained in:
2026-06-01 16:11:29 +02:00
parent 5557650824
commit 3424c1c9f7
21 changed files with 5010 additions and 47 deletions
+505
View File
@@ -0,0 +1,505 @@
/* =============================================================================
Glastree Installer - Styles
============================================================================= */
:root {
--primary: #007bff;
--primary-dark: #0056b3;
--success: #28a745;
--danger: #dc3545;
--warning: #ffc107;
--info: #17a2b8;
--dark: #343a40;
--light: #f8f9fa;
--border: #dee2e6;
--text: #212529;
--text-muted: #6c757d;
--radius: 8px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 15px;
line-height: 1.6;
color: var(--text);
background: #e9ecef;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.installer-header {
background: linear-gradient(135deg, #343a40 0%, #212529 100%);
color: #fff;
padding: 30px 0;
text-align: center;
}
.installer-header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 4px;
}
.installer-header .subtitle {
font-size: 14px;
opacity: 0.75;
margin: 0;
}
/* Step indicator */
.step-indicator {
text-align: center;
padding: 12px 0;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Card */
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card-body {
padding: 28px;
}
.card-body h2 {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.card-body h4 {
font-size: 17px;
font-weight: 600;
margin-bottom: 12px;
color: var(--dark);
}
.card-body h5 {
font-size: 15px;
font-weight: 600;
margin-bottom: 8px;
}
.card-body p {
color: var(--text-muted);
margin-bottom: 20px;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 24px 0;
}
/* Form elements */
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 14px;
color: var(--dark);
}
.form-control {
width: 100%;
padding: 10px 14px;
font-size: 15px;
border: 1px solid var(--border);
border-radius: 6px;
background: #fff;
transition: border-color 0.2s;
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0,123,255,0.15);
}
.form-text {
display: block;
margin-top: 4px;
font-size: 13px;
color: var(--text-muted);
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 22px;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: #fff;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: #6c757d;
color: #fff;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-lg {
padding: 14px 32px;
font-size: 17px;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: space-between;
align-items: center;
margin-top: 24px;
}
/* Option cards */
.option-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 20px 0;
}
.option-card {
display: block;
position: relative;
border: 2px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.option-card:hover {
border-color: var(--primary);
box-shadow: 0 2px 8px rgba(0,123,255,0.1);
}
.option-card.selected {
border-color: var(--primary);
background: rgba(0,123,255,0.03);
}
.option-card .option-radio {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.option-content {
padding: 20px;
}
.option-icon {
font-size: 32px;
margin-bottom: 8px;
color: var(--primary);
}
.option-card h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 6px;
}
.option-card p {
font-size: 13px;
line-height: 1.5;
}
.option-card ul {
margin-top: 8px;
padding-left: 18px;
font-size: 13px;
color: var(--text-muted);
}
.option-card ul li {
margin-bottom: 2px;
}
/* Requirements */
.requirements-list {
margin: 16px 0;
}
.requirement-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 6px;
font-size: 14px;
}
.requirement-item.passed {
background: #f0fff4;
border-color: #b8e6c8;
}
.requirement-item.failed {
background: #fff5f5;
border-color: #f5c6cb;
}
.req-icon {
font-size: 18px;
width: 24px;
text-align: center;
}
.requirement-item.passed .req-icon { color: var(--success); }
.requirement-item.failed .req-icon { color: var(--danger); }
.req-name {
flex: 1;
font-weight: 600;
}
.req-status {
font-size: 12px;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
}
.requirement-item.passed .req-status {
background: #d4edda;
color: #155724;
}
.requirement-item.failed .req-status {
background: #f8d7da;
color: #721c24;
}
.req-msg {
width: 100%;
margin-top: 4px;
font-size: 13px;
color: var(--danger);
}
/* Alerts */
.alert {
padding: 14px 18px;
border-radius: 6px;
margin-bottom: 16px;
font-size: 14px;
line-height: 1.5;
}
.alert-danger {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.alert-success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.alert-warning {
background: #fff3cd;
border: 1px solid #ffeeba;
color: #856404;
}
.alert-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
.alert ul {
margin-top: 8px;
padding-left: 20px;
}
/* Log list */
.log-list {
margin: 12px 0;
}
.log-item {
padding: 6px 12px;
font-size: 13px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
background: var(--light);
border-left: 3px solid var(--success);
margin-bottom: 2px;
}
/* Results list */
.results-list {
margin: 16px 0;
}
.result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 6px;
margin-bottom: 4px;
font-size: 14px;
}
.result-item.success {
background: #f0fff4;
color: #155724;
}
.result-item.error {
background: #fff5f5;
color: #721c24;
}
.result-icon {
font-size: 18px;
}
/* Final box */
.final-box {
background: var(--light);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 20px;
}
.final-box h3 {
font-size: 17px;
font-weight: 700;
margin-bottom: 12px;
}
.final-box ol,
.final-box ul {
padding-left: 20px;
line-height: 1.8;
}
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 13px;
background: #f4f4f4;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
code.d-block {
display: block;
background: #f4f4f4;
border-radius: 6px;
font-size: 13px;
}
.bg-light {
background: var(--light);
}
.p-2 {
padding: 12px;
}
.p-3 {
padding: 16px;
}
.mt-2 { margin-top: 8px; }
.mt-3 { margin-top: 16px; }
.mb-0 { margin-bottom: 0; }
.mb-2 { margin-bottom: 8px; }
.border {
border: 1px solid var(--border);
}
.rounded {
border-radius: 6px;
}
.badge-admin {
background: #007bff;
color: #fff;
font-size: 11px;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
}
/* Icons via unicode/emoji fallback since no icon font */
.icon-ok::before { content: "\2714 "; }
.icon-error::before { content: "\2718 "; }
.icon-warning::before { content: "\26A0 "; }
.icon-rocket::before { content: "\1F680 "; }
.icon-upload::before { content: "\2191 "; }
.icon-tree::before { content: "\2328 "; }
/* Responsive */
@media (max-width: 600px) {
.option-group {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.form-actions .btn {
width: 100%;
text-align: center;
}
}
/* Footer */
.installer-footer {
text-align: center;
padding: 16px 0;
color: var(--text-muted);
font-size: 13px;
}
+533
View File
@@ -0,0 +1,533 @@
<?php
declare(strict_types=1);
// =============================================================================
// Glastree Installer - Helper Functions
// =============================================================================
function getBaseUrl(): string
{
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
$scriptDir = str_replace('/installer', '', $scriptDir);
return $scheme . '://' . $host . $scriptDir;
}
function siteUrl(string $path = ''): string
{
return rtrim(getBaseUrl(), '/') . '/' . ltrim($path, '/');
}
function redirect(string $url): never
{
header('Location: ' . $url);
exit;
}
function isEnvFileExists(): bool
{
return file_exists(dirname(__DIR__) . '/.env');
}
function e(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function old(string $key, mixed $default = ''): string
{
return e($_POST[$key] ?? $_SESSION['form_data'][$key] ?? $default);
}
function checkRequirements(): array
{
$checks = [];
// PHP version
$checks[] = [
'name' => 'PHP 8.2+',
'passed' => PHP_VERSION_ID >= 80200,
'message' => PHP_VERSION_ID >= 80200 ? 'PHP ' . PHP_VERSION : 'Versione attuale: PHP ' . PHP_VERSION . '. Necessaria 8.2+',
'critical' => true,
];
// Extensions
$extensions = [
'pdo_mysql' => 'PDO MySQL',
'mbstring' => 'MBString',
'xml' => 'XML',
'curl' => 'cURL',
'zip' => 'Zip',
'gd' => 'GD (immagini)',
'fileinfo' => 'Fileinfo',
];
foreach ($extensions as $ext => $label) {
$checks[] = [
'name' => $label,
'passed' => extension_loaded($ext),
'message' => extension_loaded($ext) ? 'Caricata' : 'Estensione "' . $ext . '" non trovata',
'critical' => true,
];
}
// Directory permissions
$dirs = [
'storage' => dirname(__DIR__) . '/storage',
'bootstrap/cache' => dirname(__DIR__) . '/bootstrap/cache',
];
foreach ($dirs as $label => $dir) {
$writable = is_dir($dir) && is_writable($dir);
$checks[] = [
'name' => $label . '/ scrivibile',
'passed' => $writable,
'message' => $writable ? 'Permessi OK' : 'Cartella non scrivibile. Esegui: chmod -R 775 ' . $dir,
'critical' => true,
];
}
// mod_rewrite (suggested)
$checks[] = [
'name' => 'mod_rewrite (Apache)',
'passed' => true,
'message' => 'Raccomandato per URL puliti. Verifica configurazione Apache.',
'critical' => false,
];
// HTTPS
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
$checks[] = [
'name' => 'HTTPS attivo',
'passed' => $isHttps,
'message' => $isHttps ? 'Connessione sicura' : 'Connessione non crittata. Configura SSL in produzione.',
'critical' => false,
];
return $checks;
}
function allRequirementsMet(array $checks): bool
{
foreach ($checks as $check) {
if (!empty($check['critical']) && !$check['passed']) {
return false;
}
}
return true;
}
function testDatabaseConnection(string $host, int $port, string $database, string $user, string $pass): array
{
try {
$dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
return ['success' => true, 'message' => 'Connessione riuscita.'];
} catch (PDOException $e) {
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
}
}
function testRootConnection(string $host, int $port, string $user, string $pass): array
{
try {
$dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5,
]);
return ['success' => true, 'message' => 'Connessione root riuscita.', 'pdo' => $pdo];
} catch (PDOException $e) {
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
}
}
function createDatabaseAndUser(string $rootUser, string $rootPass, string $host, int $port, string $dbName, string $dbUser, string $dbPass): array
{
$result = testRootConnection($host, $port, $rootUser, $rootPass);
if (!$result['success']) {
return $result;
}
try {
$pdo = $result['pdo'];
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("CREATE USER IF NOT EXISTS '{$dbUser}'@'localhost' IDENTIFIED BY '{$dbPass}'");
$pdo->exec("GRANT ALL ON `{$dbName}`.* TO '{$dbUser}'@'localhost'");
$pdo->exec("FLUSH PRIVILEGES");
return ['success' => true, 'message' => 'Database e utente creati con successo.'];
} catch (PDOException $e) {
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
}
}
function writeEnvFile(array $params): bool
{
$rootDir = dirname(__DIR__);
$examplePath = $rootDir . '/.env.example';
$envPath = $rootDir . '/.env';
if (!file_exists($examplePath)) {
return false;
}
$content = file_get_contents($examplePath);
$replacements = [
'APP_NAME=' => 'APP_NAME=' . ($params['app_name'] ?? 'Glastree'),
'APP_ENV=' => 'APP_ENV=production',
'APP_KEY=' => 'APP_KEY=',
'APP_DEBUG=' => 'APP_DEBUG=false',
'APP_URL=' => 'APP_URL=' . ($params['app_url'] ?? 'http://localhost'),
'DB_CONNECTION=' => 'DB_CONNECTION=mysql',
'# DB_HOST=' => 'DB_HOST=',
'# DB_PORT=' => 'DB_PORT=',
'# DB_DATABASE=' => 'DB_DATABASE=',
'# DB_USERNAME=' => 'DB_USERNAME=',
'# DB_PASSWORD=' => 'DB_PASSWORD=',
];
foreach ($replacements as $search => $replace) {
if (str_starts_with($content, $search) || preg_match('/^' . preg_quote($search, '/') . '/m', $content)) {
$content = preg_replace(
'/^' . preg_quote($search, '/') . '.*$/m',
$replace . ($params['db_host'] ?? '127.0.0.1'),
$content
);
}
}
// Specific DB replacements (handle commented lines)
$content = preg_replace(
'/^# DB_HOST=.*$/m',
'DB_HOST=' . ($params['db_host'] ?? '127.0.0.1'),
$content
);
$content = preg_replace(
'/^# DB_PORT=.*$/m',
'DB_PORT=' . ($params['db_port'] ?? '3306'),
$content
);
$content = preg_replace(
'/^# DB_DATABASE=.*$/m',
'DB_DATABASE=' . ($params['db_database'] ?? 'glastree'),
$content
);
$content = preg_replace(
'/^# DB_USERNAME=.*$/m',
'DB_USERNAME=' . ($params['db_username'] ?? 'glastree'),
$content
);
$content = preg_replace(
'/^# DB_PASSWORD=.*$/m',
'DB_PASSWORD=' . ($params['db_password'] ?? ''),
$content
);
// Ensure APP_KEY= line exists
if (!preg_match('/^APP_KEY=/m', $content)) {
$content .= "\nAPP_KEY=\n";
}
return file_put_contents($envPath, $content) !== false;
}
function bootstrapLaravel(): object
{
$rootDir = dirname(__DIR__);
require_once $rootDir . '/vendor/autoload.php';
$app = require $rootDir . '/bootstrap/app.php';
$kernel = $app->make(\Illuminate\Contracts\Console\Kernel::class);
return $kernel;
}
function runArtisanCommands(object $kernel, array $commands): array
{
$results = [];
foreach ($commands as $command => $options) {
try {
$exitCode = $kernel->call($command, $options ?? []);
$results[] = [
'command' => $command,
'success' => $exitCode === 0,
'output' => $kernel->output(),
];
} catch (\Exception $e) {
$results[] = [
'command' => $command,
'success' => false,
'output' => $e->getMessage(),
];
}
}
return $results;
}
function restoreFromBackup(string $zipPath, string $dbUser, string $dbPass, string $dbName): array
{
$rootDir = dirname(__DIR__);
$restoreDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid();
if (!file_exists($zipPath)) {
return ['success' => false, 'message' => 'File ZIP non trovato: ' . $zipPath];
}
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
return ['success' => false, 'message' => 'Impossibile aprire il file ZIP.'];
}
$zip->extractTo($restoreDir);
$zip->close();
$log = [];
// 1. Import database.sql
$sqlFile = $restoreDir . '/database.sql';
if (file_exists($sqlFile)) {
$cmd = sprintf(
'mysql --host=%s --port=%s --user=%s --password=%s %s < %s 2>&1',
escapeshellarg($_SESSION['db_host'] ?? '127.0.0.1'),
escapeshellarg((string)($_SESSION['db_port'] ?? '3306')),
escapeshellarg($dbUser),
escapeshellarg($dbPass),
escapeshellarg($dbName),
escapeshellarg($sqlFile)
);
exec($cmd, $output, $exitCode);
if ($exitCode === 0) {
$log[] = 'Database importato con successo.';
} else {
$log[] = 'Errore import database: ' . implode("\n", $output);
return ['success' => false, 'message' => 'Import database fallito.', 'log' => $log];
}
} else {
$log[] = 'database.sql non trovato nel backup.';
}
// 2. Copy files to storage
$filesDir = $restoreDir . '/files';
$targetDir = $rootDir . '/storage/app/documenti';
if (is_dir($filesDir)) {
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($filesDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($files as $file) {
$relative = substr($file->getPathname(), strlen($filesDir) + 1);
$target = $targetDir . '/' . $relative;
if ($file->isDir()) {
if (!is_dir($target)) {
mkdir($target, 0755, true);
}
} else {
copy($file->getPathname(), $target);
}
}
$log[] = 'File documenti ripristinati.';
} else {
$log[] = 'Cartella files non trovata nel backup.';
}
// 3. Merge .env if present (preserving current DB credentials)
$envBackup = $restoreDir . '/.env';
if (file_exists($envBackup)) {
$currentEnv = file_get_contents($envBackup);
$currentEnv = preg_replace('/^DB_HOST=.*$/m', 'DB_HOST=' . ($_SESSION['db_host'] ?? '127.0.0.1'), $currentEnv);
$currentEnv = preg_replace('/^DB_PORT=.*$/m', 'DB_PORT=' . ($_SESSION['db_port'] ?? '3306'), $currentEnv);
$currentEnv = preg_replace('/^DB_DATABASE=.*$/m', 'DB_DATABASE=' . $dbName, $currentEnv);
$currentEnv = preg_replace('/^DB_USERNAME=.*$/m', 'DB_USERNAME=' . $dbUser, $currentEnv);
$currentEnv = preg_replace('/^DB_PASSWORD=.*$/m', 'DB_PASSWORD=' . $dbPass, $currentEnv);
// Only merge if APP_URL differs from localhost
if (str_contains($currentEnv, 'APP_URL=http://localhost') || str_contains($currentEnv, 'APP_URL=')) {
$currentEnv = preg_replace(
'/^APP_URL=.*$/m',
'APP_URL=' . getBaseUrl(),
$currentEnv
);
}
file_put_contents($rootDir . '/.env', $currentEnv);
$log[] = '.env ripristinato dal backup (credenziali DB preservate).';
}
// 4. Copy manifest if exists
if (file_exists($restoreDir . '/manifest.json')) {
$log[] = 'Manifest: ' . file_get_contents($restoreDir . '/manifest.json');
}
// Cleanup temp
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($restoreDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file) {
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($restoreDir);
return ['success' => true, 'message' => 'Backup ripristinato con successo.', 'log' => $log];
}
function createSuperAdmin(string $name, string $email, string $password): array
{
try {
$kernel = bootstrapLaravel();
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\DiocesiSeeder']);
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\ComuniSeeder']);
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\RoleSeeder']);
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\RolePresetSeeder']);
} catch (\Exception $e) {
// Seeder might fail if already seeded, that's OK
}
try {
$user = new \App\Models\User();
$user->name = $name;
$user->email = $email;
$user->password = \Illuminate\Support\Facades\Hash::make($password);
$user->is_admin = true;
$user->status = 'active';
$user->role_preset_id = 1;
$user->permissions = [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 2,
'mailing' => 2,
'viste' => 2,
'report' => 2,
'settings' => 2,
];
$user->save();
return ['success' => true, 'message' => 'Amministratore creato con successo.'];
} catch (\Exception $e) {
return ['success' => false, 'message' => 'Errore creazione admin: ' . $e->getMessage()];
}
}
function getExistingUsers(): array
{
try {
$kernel = bootstrapLaravel();
$users = \App\Models\User::all(['id', 'name', 'email', 'is_admin'])->toArray();
return $users;
} catch (\Exception $e) {
return [];
}
}
function selfDestruct(): bool
{
$installerDir = __DIR__;
if (!is_dir($installerDir)) {
return false;
}
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($installerDir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file) {
if ($file->isDir()) {
rmdir($file->getPathname());
} else {
unlink($file->getPathname());
}
}
rmdir($installerDir);
return !is_dir($installerDir);
}
function renderHeader(string $title, string $step = ''): void
{
$stepLabel = $step ? " — Step {$step}" : '';
?>
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($title) ?> — Glastree Installer</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="installer-wrapper">
<div class="installer-header">
<div class="container">
<h1><i class="icon-tree"></i> Glastree</h1>
<p class="subtitle">Installazione Wizard</p>
</div>
</div>
<div class="container">
<?php if ($step): ?>
<div class="step-indicator">Passo <?= e($step) ?> di 6</div>
<?php endif; ?>
<div class="card">
<div class="card-body">
<?php
}
function renderFooter(): void
{
?>
</div>
</div>
</div>
<div class="installer-footer">
<div class="container">
<p>Glastree &copy; <?= date('Y') ?></p>
</div>
</div>
</div>
</body>
</html>
<?php
}
function renderErrors(array $errors): void
{
if (empty($errors)) return;
?>
<div class="alert alert-danger">
<strong><i class="icon-error"></i> Errori:</strong>
<ul class="mb-0">
<?php foreach ($errors as $error): ?>
<li><?= e($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php
}
function renderSuccess(string $message, array $log = []): void
{
?>
<div class="alert alert-success">
<strong><i class="icon-ok"></i> <?= e($message) ?></strong>
<?php if (!empty($log)): ?>
<ul class="mb-0 mt-2">
<?php foreach ($log as $item): ?>
<li><?= e($item) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
// =============================================================================
// Glastree - Installer Entry Point
// =============================================================================
// Se .env esiste già, l'app è installata → redirect a login.
// Altrimenti avvia il wizard di installazione.
// =============================================================================
$rootDir = dirname(__DIR__);
if (file_exists($rootDir . '/.env')) {
// App gia' installata
$baseUrl = rtrim((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . dirname($_SERVER['SCRIPT_NAME']), '/');
$baseUrl = str_replace('/installer', '', $baseUrl);
header('Location: ' . $baseUrl . '/login');
exit;
}
// Avvia il wizard
session_start();
$_SESSION['install_mode'] = $_SESSION['install_mode'] ?? '';
$_SESSION['install_step'] = 1;
header('Location: setup.php?step=1');
exit;
+711
View File
@@ -0,0 +1,711 @@
<?php
declare(strict_types=1);
// =============================================================================
// Glastree Installer - Wizard Setup
// =============================================================================
session_start();
require_once __DIR__ . '/functions.php';
// If already installed (and not in the middle of install), block
if (isEnvFileExists() && empty($_SESSION['install_in_progress'])) {
redirect('/login');
}
$step = max(1, min(6, (int) ($_GET['step'] ?? 1)));
$errors = [];
$success = '';
// ─── Step 1: Welcome + Mode Selection ──────────────────────────────────────
if ($step === 1) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$mode = $_POST['mode'] ?? '';
if (!in_array($mode, ['fresh', 'backup'], true)) {
$errors[] = 'Seleziona una modalita di installazione.';
} else {
$_SESSION['install_mode'] = $mode;
$_SESSION['install_in_progress'] = true;
redirect('setup.php?step=2');
}
}
renderHeader('Benvenuto', '1');
?>
<h2>Benvenuto nell'installazione di Glastree</h2>
<p>Questo wizard ti guidera nella configurazione dell'applicazione su questo server.</p>
<?php renderErrors($errors); ?>
<form method="POST">
<div class="option-group">
<label class="option-card <?= ($_SESSION['install_mode'] ?? '') === 'fresh' ? 'selected' : '' ?>">
<input type="radio" name="mode" value="fresh" class="option-radio"
<?= ($_SESSION['install_mode'] ?? '') === 'fresh' ? 'checked' : '' ?>>
<div class="option-content">
<div class="option-icon"><i class="icon-rocket"></i></div>
<h3>Fresh Install</h3>
<p>Crea un database nuovo, esegue migration e seed, e configura l'applicazione da zero.</p>
<ul>
<li>Database nuovo</li>
<li>Migration + dati di base</li>
<li>Crea amministratore</li>
</ul>
</div>
</label>
<label class="option-card <?= ($_SESSION['install_mode'] ?? '') === 'backup' ? 'selected' : '' ?>">
<input type="radio" name="mode" value="backup" class="option-radio"
<?= ($_SESSION['install_mode'] ?? '') === 'backup' ? 'checked' : '' ?>>
<div class="option-content">
<div class="option-icon"><i class="icon-upload"></i></div>
<h3>Restore da Backup</h3>
<p>Carica un file ZIP di backup per ripristinare un'installazione esistente su questo server.</p>
<ul>
<li>Upload file ZIP</li>
<li>Importa database</li>
<li>Ripristina documenti</li>
</ul>
</div>
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Avanti &rarr;</button>
</div>
</form>
<?php
renderFooter();
}
// ─── Step 2: Requirements Check ─────────────────────────────────────────────
if ($step === 2) {
$checks = checkRequirements();
$allMet = allRequirementsMet($checks);
$nonCriticalFailed = false;
foreach ($checks as $check) {
if (!$check['passed'] && empty($check['critical'])) {
$nonCriticalFailed = true;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $allMet) {
redirect('setup.php?step=3');
}
renderHeader('Verifica Requisiti', '2');
?>
<h2>Verifica Requisiti di Sistema</h2>
<p>Il sistema controlla che il server soddisfi i requisiti minimi per eseguire Glastree.</p>
<?php renderErrors($errors); ?>
<div class="requirements-list">
<?php foreach ($checks as $check): ?>
<div class="requirement-item <?= $check['passed'] ? 'passed' : 'failed' ?>">
<span class="req-icon">
<?php if ($check['passed']): ?>
<i class="icon-ok"></i>
<?php else: ?>
<i class="icon-error"></i>
<?php endif; ?>
</span>
<span class="req-name"><?= e($check['name']) ?></span>
<span class="req-status"><?= $check['passed'] ? 'OK' : 'ERRORE' ?></span>
<?php if (!$check['passed']): ?>
<span class="req-msg"><?= e($check['message']) ?></span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if (!$allMet): ?>
<div class="alert alert-danger">
<i class="icon-error"></i>
Requisiti critici non soddisfatti. Risolvi gli errori sopra e ricarica la pagina.
</div>
<?php elseif ($nonCriticalFailed): ?>
<div class="alert alert-warning">
<i class="icon-warning"></i>
Alcuni requisiti non critici non sono soddisfatti. Puoi proseguire, ma alcune funzionalita potrebbero essere limitate.
</div>
<?php endif; ?>
<form method="POST">
<div class="form-actions">
<a href="setup.php?step=1" class="btn btn-secondary">&larr; Indietro</a>
<?php if ($allMet): ?>
<button type="submit" class="btn btn-primary">Avanti &rarr;</button>
<?php endif; ?>
</div>
</form>
<?php
renderFooter();
}
// ─── Step 3: Database Configuration ─────────────────────────────────────────
if ($step === 3) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$dbHost = $_POST['db_host'] ?? '127.0.0.1';
$dbPort = (int) ($_POST['db_port'] ?? 3306);
if ($action === 'test_root') {
// Test root credentials
$rootUser = $_POST['root_user'] ?? 'root';
$rootPass = $_POST['root_pass'] ?? '';
$result = testRootConnection($dbHost, $dbPort, $rootUser, $rootPass);
if ($result['success']) {
$_SESSION['root_user'] = $rootUser;
$_SESSION['root_pass'] = $rootPass;
$success = 'Connessione root riuscita. Ora puoi creare il database.';
} else {
$errors[] = $result['message'];
}
} elseif ($action === 'create_db') {
// Create database + user
$rootUser = $_SESSION['root_user'] ?? 'root';
$rootPass = $_SESSION['root_pass'] ?? '';
$dbName = $_POST['db_name'] ?? 'glastree';
$dbUser = $_POST['db_user'] ?? 'glastree';
$dbPass = $_POST['db_pass'] ?? '';
$appUrl = $_POST['app_url'] ?? getBaseUrl();
if (strlen($dbPass) < 6) {
$errors[] = 'La password del database deve essere almeno 6 caratteri.';
} else {
$result = createDatabaseAndUser($rootUser, $rootPass, $dbHost, $dbPort, $dbName, $dbUser, $dbPass);
if ($result['success']) {
$_SESSION['db_host'] = $dbHost;
$_SESSION['db_port'] = $dbPort;
$_SESSION['db_name'] = $dbName;
$_SESSION['db_user'] = $dbUser;
$_SESSION['db_pass'] = $dbPass;
$_SESSION['app_url'] = $appUrl;
// Write .env
$envWritten = writeEnvFile([
'app_url' => $appUrl,
'db_host' => $dbHost,
'db_port' => (string) $dbPort,
'db_database' => $dbName,
'db_username' => $dbUser,
'db_password' => $dbPass,
]);
if ($envWritten) {
redirect('setup.php?step=4');
} else {
$errors[] = 'Impossibile scrivere il file .env.';
}
} else {
$errors[] = $result['message'];
}
}
} elseif ($action === 'test_existing') {
// Test existing database connection
$dbName = $_POST['db_name'] ?? '';
$dbUser = $_POST['db_user'] ?? '';
$dbPass = $_POST['db_pass'] ?? '';
$appUrl = $_POST['app_url'] ?? getBaseUrl();
if (empty($dbName) || empty($dbUser)) {
$errors[] = 'Compila tutti i campi obbligatori.';
} else {
$result = testDatabaseConnection($dbHost, $dbPort, $dbName, $dbUser, $dbPass);
if ($result['success']) {
$_SESSION['db_host'] = $dbHost;
$_SESSION['db_port'] = $dbPort;
$_SESSION['db_name'] = $dbName;
$_SESSION['db_user'] = $dbUser;
$_SESSION['db_pass'] = $dbPass;
$_SESSION['app_url'] = $appUrl;
$envWritten = writeEnvFile([
'app_url' => $appUrl,
'db_host' => $dbHost,
'db_port' => (string) $dbPort,
'db_database' => $dbName,
'db_username' => $dbUser,
'db_password' => $dbPass,
]);
if ($envWritten) {
redirect('setup.php?step=4');
} else {
$errors[] = 'Impossibile scrivere il file .env.';
}
} else {
$errors[] = $result['message'];
}
}
} else {
$errors[] = 'Azione non valida.';
}
}
renderHeader('Configurazione Database', '3');
?>
<h2>Configurazione Database</h2>
<p>Configura la connessione al database MySQL.</p>
<?php renderErrors($errors); ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="icon-ok"></i> <?= e($success) ?></div>
<?php endif; ?>
<form method="POST" id="db-form">
<div class="form-group">
<label for="db_host">Host MySQL</label>
<input type="text" name="db_host" id="db_host" class="form-control" value="<?= old('db_host', '127.0.0.1') ?>" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label for="db_port">Porta</label>
<input type="number" name="db_port" id="db_port" class="form-control" value="<?= old('db_port', '3306') ?>" placeholder="3306">
</div>
<div class="form-group">
<label for="app_url">URL Applicazione</label>
<input type="text" name="app_url" id="app_url" class="form-control" value="<?= old('app_url', getBaseUrl()) ?>" placeholder="https://glastree.esempio.it">
<small class="form-text text-muted">L'indirizzo pubblico con cui si accedera a Glastree</small>
</div>
<hr>
<h4>Modalita A: Crea Nuovo Database</h4>
<div class="form-group">
<label for="root_user">Utente Root MySQL</label>
<input type="text" name="root_user" id="root_user" class="form-control" value="<?= old('root_user', 'root') ?>">
</div>
<div class="form-group">
<label for="root_pass">Password Root</label>
<input type="password" name="root_pass" id="root_pass" class="form-control" value="">
</div>
<button type="submit" name="action" value="test_root" class="btn btn-secondary">
<i class="icon-ok"></i> Test Connessione Root
</button>
<div class="mt-3 p-3 bg-light border rounded">
<h5>Nuovo Database da Creare</h5>
<div class="form-group">
<label for="db_name">Nome Database</label>
<input type="text" name="db_name" class="form-control" value="<?= old('db_name', 'glastree') ?>" placeholder="glastree">
</div>
<div class="form-group">
<label for="db_user">Utente Database</label>
<input type="text" name="db_user" class="form-control" value="<?= old('db_user', 'glastree') ?>" placeholder="glastree">
</div>
<div class="form-group">
<label for="db_pass">Password Utente</label>
<input type="password" name="db_pass" class="form-control" value="">
</div>
<button type="submit" name="action" value="create_db" class="btn btn-primary">
<i class="icon-rocket"></i> Crea Database e Continua
</button>
</div>
<hr>
<h4>Modalita B: Usa Database Esistente</h4>
<div class="form-group">
<label for="db_name_ex">Nome Database</label>
<input type="text" name="db_name" class="form-control" value="<?= old('db_name', 'glastree') ?>" placeholder="glastree">
</div>
<div class="form-group">
<label for="db_user_ex">Utente</label>
<input type="text" name="db_user" class="form-control" value="<?= old('db_user', 'glastree') ?>" placeholder="glastree">
</div>
<div class="form-group">
<label for="db_pass_ex">Password</label>
<input type="password" name="db_pass" class="form-control" value="">
</div>
<button type="submit" name="action" value="test_existing" class="btn btn-primary">
<i class="icon-ok"></i> Test e Continua
</button>
</form>
<div class="form-actions mt-3">
<a href="setup.php?step=2" class="btn btn-secondary">&larr; Indietro</a>
</div>
<?php
renderFooter();
}
// ─── Step 4: Install (Fresh: migrate+seed / Backup: upload+restore) ────────
if ($step === 4) {
$mode = $_SESSION['install_mode'] ?? 'fresh';
$installDone = $_SESSION['install_done'] ?? false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($mode === 'fresh') {
// Run Artisan commands
try {
$kernel = bootstrapLaravel();
$commands = [
'key:generate' => ['--force' => true],
'migrate' => ['--force' => true],
'db:seed' => ['--force' => true],
'storage:link' => [],
];
$results = runArtisanCommands($kernel, $commands);
$allOk = true;
$log = [];
foreach ($results as $r) {
$log[] = ($r['success'] ? 'OK' : 'ERRORE') . ': ' . $r['command'];
if (!$r['success']) $allOk = false;
}
if ($allOk) {
$_SESSION['install_done'] = true;
$success = 'Installazione completata con successo!';
$log[] = 'Ora puoi creare l\'amministratore.';
} else {
$errors[] = 'Alcuni comandi sono falliti. Verifica i log.';
foreach ($log as $l) $errors[] = $l;
}
} catch (\Exception $e) {
$errors[] = 'Errore: ' . $e->getMessage();
}
} elseif ($mode === 'backup') {
// Handle backup upload
if (!empty($_FILES['backup_file']) && $_FILES['backup_file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['backup_file']['tmp_name'];
$originalName = $_FILES['backup_file']['name'];
$result = restoreFromBackup(
$uploadedFile,
$_SESSION['db_user'] ?? '',
$_SESSION['db_pass'] ?? '',
$_SESSION['db_name'] ?? ''
);
if ($result['success']) {
$_SESSION['install_done'] = true;
$success = 'Backup ripristinato con successo!';
$log = $result['log'] ?? [];
} else {
$errors[] = $result['message'];
if (!empty($result['log'])) {
foreach ($result['log'] as $l) $errors[] = $l;
}
}
} elseif (!empty($_POST['backup_path'])) {
// Backup file already on server
$serverPath = $_POST['backup_path'];
if (!file_exists($serverPath)) {
$errors[] = 'File non trovato: ' . $serverPath;
} else {
$result = restoreFromBackup(
$serverPath,
$_SESSION['db_user'] ?? '',
$_SESSION['db_pass'] ?? '',
$_SESSION['db_name'] ?? ''
);
if ($result['success']) {
$_SESSION['install_done'] = true;
$success = 'Backup ripristinato con successo!';
} else {
$errors[] = $result['message'];
}
}
} else {
$errors[] = 'Seleziona un file di backup o fornisci il percorso.';
}
}
}
renderHeader($mode === 'fresh' ? 'Installazione' : 'Ripristino Backup', '4');
?>
<h2><?= $mode === 'fresh' ? 'Installazione Database' : 'Ripristino Backup' ?></h2>
<?php renderErrors($errors); ?>
<?php if ($success): ?>
<div class="alert alert-success"><i class="icon-ok"></i> <?= e($success) ?></div>
<?php if (!empty($log)): ?>
<div class="log-list">
<?php foreach ($log as $l): ?>
<div class="log-item"><?= e($l) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($installDone): ?>
<div class="alert alert-info">
<i class="icon-ok"></i> Installazione completata. Procedi al passo successivo.
</div>
<div class="form-actions">
<a href="setup.php?step=5" class="btn btn-primary">Avanti &rarr;</a>
</div>
<?php else: ?>
<?php if ($mode === 'fresh'): ?>
<form method="POST">
<p>Clicca "Installa" per eseguire migration, seed e generare la chiave dell'applicazione.</p>
<div class="form-actions">
<a href="setup.php?step=3" class="btn btn-secondary">&larr; Indietro</a>
<button type="submit" class="btn btn-primary"><i class="icon-rocket"></i> Installa</button>
</div>
</form>
<?php else: ?>
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="backup_file">Carica file ZIP di backup</label>
<input type="file" name="backup_file" id="backup_file" class="form-control" accept=".zip">
</div>
<hr>
<p class="text-muted">oppure specifica il percorso sul server:</p>
<div class="form-group">
<label for="backup_path">Percorso server del file ZIP</label>
<input type="text" name="backup_path" id="backup_path" class="form-control" placeholder="/tmp/backup.zip">
</div>
<div class="form-actions">
<a href="setup.php?step=3" class="btn btn-secondary">&larr; Indietro</a>
<button type="submit" class="btn btn-primary"><i class="icon-upload"></i> Ripristina Backup</button>
</div>
</form>
<?php endif; ?>
<?php endif; ?>
<?php
renderFooter();
}
// ─── Step 5: Admin User Creation ────────────────────────────────────────────
if ($step === 5) {
$mode = $_SESSION['install_mode'] ?? 'fresh';
$adminCreated = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($mode === 'fresh') {
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$passwordConfirm = $_POST['password_confirm'] ?? '';
if (empty($name)) $errors[] = 'Il nome e obbligatorio.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email non valida.';
if (strlen($password) < 8) $errors[] = 'La password deve essere almeno 8 caratteri.';
if ($password !== $passwordConfirm) $errors[] = 'Le password non coincidono.';
if (empty($errors)) {
$result = createSuperAdmin($name, $email, $password);
if ($result['success']) {
$adminCreated = true;
$_SESSION['admin_created'] = true;
} else {
$errors[] = $result['message'];
}
}
} else {
// Backup mode: skip or create additional admin
if (!empty($_POST['name']) && !empty($_POST['email']) && !empty($_POST['password'])) {
$result = createSuperAdmin($_POST['name'], $_POST['email'], $_POST['password']);
if ($result['success']) {
$adminCreated = true;
} else {
$errors[] = $result['message'];
}
} else {
$_SESSION['admin_created'] = true;
$adminCreated = true;
}
}
}
// Check existing users (backup mode)
$existingUsers = [];
if ($mode === 'backup' && file_exists(dirname(__DIR__) . '/.env')) {
try {
$existingUsers = getExistingUsers();
} catch (\Exception $e) {
// Ignore
}
}
renderHeader('Amministratore', '5');
?>
<h2><?= $mode === 'fresh' ? 'Crea Amministratore' : 'Verifica Utenti' ?></h2>
<?php renderErrors($errors); ?>
<?php if ($success): ?>
<div class="alert alert-success"><?= e($success) ?></div>
<?php endif; ?>
<?php if ($adminCreated || !empty($_SESSION['admin_created'])): ?>
<div class="alert alert-success">
<i class="icon-ok"></i>
<?php if ($mode === 'fresh'): ?>
Amministratore creato con successo!
<?php else: ?>
Utenti configurati.
<?php endif; ?>
</div>
<div class="form-actions">
<a href="setup.php?step=6" class="btn btn-primary">Avanti &rarr;</a>
</div>
<?php else: ?>
<?php if (!empty($existingUsers)): ?>
<div class="alert alert-info">
<strong>Utenti trovati nel database ripristinato:</strong>
<ul>
<?php foreach ($existingUsers as $u): ?>
<li><?= e($u['name']) ?> &lt;<?= e($u['email']) ?>&gt;
<?= $u['is_admin'] ? '<span class="badge badge-admin">Superadmin</span>' : '' ?>
</li>
<?php endforeach; ?>
</ul>
<p>Puoi proseguire senza creare nuovi utenti.</p>
</div>
<form method="POST" class="mt-3">
<button type="submit" class="btn btn-secondary">Salta (usa utenti esistenti)</button>
</form>
<hr>
<p class="text-muted">oppure crea un nuovo amministratore:</p>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="name">Nome Amministratore</label>
<input type="text" name="name" id="name" class="form-control" value="<?= old('name', 'Amministratore') ?>" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" name="email" id="email" class="form-control" value="<?= old('email', 'admin@glastree.local') ?>" required>
</div>
<div class="form-group">
<label for="password">Password (min. 8 caratteri)</label>
<input type="password" name="password" id="password" class="form-control" required minlength="8">
</div>
<div class="form-group">
<label for="password_confirm">Conferma Password</label>
<input type="password" name="password_confirm" id="password_confirm" class="form-control" required minlength="8">
</div>
<div class="form-actions">
<a href="setup.php?step=4" class="btn btn-secondary">&larr; Indietro</a>
<button type="submit" class="btn btn-primary"><i class="icon-ok"></i> Crea Amministratore</button>
</div>
</form>
<?php endif; ?>
<?php
renderFooter();
}
// ─── Step 6: Finalize ───────────────────────────────────────────────────────
if ($step === 6) {
$finalized = false;
$results = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
if (!file_exists(dirname(__DIR__) . '/.env')) {
$errors[] = 'File .env non trovato. Qualcosa e andato storto.';
} else {
$kernel = bootstrapLaravel();
$commands = [
'config:cache' => [],
'route:cache' => [],
'view:cache' => [],
];
$results = runArtisanCommands($kernel, $commands);
$allOk = true;
foreach ($results as $r) {
if (!$r['success']) $allOk = false;
}
if ($allOk) {
// Self-destruct
selfDestruct();
$finalized = true;
} else {
$errors[] = 'Alcune operazioni sono fallite.';
}
}
} catch (\Exception $e) {
$errors[] = 'Errore: ' . $e->getMessage();
}
}
if (!$finalized && empty($errors)) {
// Initial display
renderHeader('Finalizzazione', '6');
?>
<h2>Finalizzazione</h2>
<p>Completa l'installazione generando la cache e rimuovendo i file di setup.</p>
<?php renderErrors($errors); ?>
<form method="POST">
<div class="form-actions">
<a href="setup.php?step=5" class="btn btn-secondary">&larr; Indietro</a>
<button type="submit" class="btn btn-primary"><i class="icon-ok"></i> Completa Installazione</button>
</div>
</form>
<?php
renderFooter();
} else {
// Show results
renderHeader('Installazione Completata', '6');
?>
<h2>Installazione Completata!</h2>
<?php renderErrors($errors); ?>
<?php if (!empty($results)): ?>
<div class="results-list">
<?php foreach ($results as $r): ?>
<div class="result-item <?= $r['success'] ? 'success' : 'error' ?>">
<span class="result-icon"><?= $r['success'] ? '<i class="icon-ok"></i>' : '<i class="icon-error"></i>' ?></span>
<span class="result-name"><?= e($r['command']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($finalized): ?>
<div class="alert alert-success">
<i class="icon-ok"></i>
<strong>Glastree e stato installato con successo!</strong>
<p class="mb-0 mt-2">La cartella <code>installer/</code> e stata eliminata per sicurezza.</p>
</div>
<div class="final-box">
<h3>Prossimi passi:</h3>
<ol>
<li><strong>Configura SSL</strong> con Let's Encrypt per connessioni sicure</li>
<li>Accedi con le credenziali amministratore</li>
<li>Vai su <strong>Impostazioni</strong> per configurare:
<ul>
<li>Nome applicazione e logo</li>
<li>Email (SMTP e IMAP)</li>
<li>Repository remoti (Google Drive, Nextcloud)</li>
</ul>
</li>
<li>Vai su <strong>Admin</strong> per creare altri utenti</li>
<li>Configura il cron per backup automatico:
<code class="d-block mt-2 p-2 bg-light">* * * * * cd <?= e(dirname(__DIR__)) ?> && php artisan schedule:run >> /dev/null 2>&1</code>
</li>
</ol>
</div>
<div class="form-actions">
<a href="<?= e(getBaseUrl()) ?>/login" class="btn btn-primary btn-lg">
<i class="icon-ok"></i> Vai al Login &rarr;
</a>
</div>
<?php endif; ?>
<?php
renderFooter();
}
}