Compare commits

4 Commits

Author SHA1 Message Date
inext 7d7d384a0d fix 1.2.8 - logo app 2026-06-08 20:10:33 +02:00
inext 8194b8c034 fix 1.2.7 2026-06-08 20:03:14 +02:00
inext 90093a086b fix 1.2.6 2026-06-08 12:09:45 +02:00
inext cbe3470c8c 1.2.5 - fix for email settings 2026-06-08 09:47:24 +02:00
19 changed files with 687 additions and 1066 deletions
+145 -53
View File
@@ -1,70 +1,162 @@
# 🧠 MEMORY.md — Stato Progetto # 🧠 MEMORY.md — Stato Progetto
## Goal ## Obiettivo
- Implementare page-length selector su liste (Individui, Eventi, Email) App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
- Implementare CSV import per gruppi
- Implementare ICS import per eventi
- Creare script build-dist.sh per distribuzione
## Modifiche Effettuate (10 file + 3 nuovi) ## Funzionalità Implementate
### File modificati ### Page-length selector
| File | Cosa | - **4 pagine**: Individui, Eventi, Email, Gruppi
|------|------|
| `app/Http/Controllers/IndividuoController.php` | perPage support (20 default, clamp [10,20,25,50,100]) |
| `app/Http/Controllers/EventoController.php` | perPage + importIcs/importIcsStore (Sabre\VObject) |
| `app/Http/Controllers/GruppoController.php` | import/importStore/downloadTemplate CSV gruppi |
| `app/Http/Controllers/EmailController.php` | perPage support |
| `app/Models/Evento.php` | `uid_esterno` aggiunto a $fillable (bug fix) |
| `resources/views/individui/index.blade.php` | dropdown perPage in card-footer |
| `resources/views/eventi/index.blade.php` | dropdown perPage + pulsante Importa ICS |
| `resources/views/gruppi/index.blade.php` | pulsante Importa CSV |
| `resources/views/email/index.blade.php` | dropdown perPage in card-footer |
| `routes/web.php` | route gruppi/import, gruppi/template, eventi/import |
### File nuovi
| File | Cosa |
|------|------|
| `resources/views/gruppi/import.blade.php` | form upload CSV gruppi |
| `resources/views/eventi/import.blade.php` | form upload ICS eventi |
| `build-dist.sh` | script build distribuzione |
## Feature
### 1. Page-length selector
- **3 pagine**: Individui, Eventi, Email
- **Valori**: 10, 20, 25, 50, 100 (default 20) - **Valori**: 10, 20, 25, 50, 100 (default 20)
- Dropdown nel card-footer con hidden fields per preservare altri query param - Dropdown nel card-footer con campi hidden per preservare query params
- GruppoController: usa `LengthAwarePaginator` per paginare la collezione ordinata gerarchicamente
### 2. CSV Import Gruppi ### CSV Import Gruppi
- Stesso pattern di `IndividuoController` (import GET, importStore POST, downloadTemplate GET) - Stesso pattern di `IndividuoController` (import GET, importStore POST, downloadTemplate GET)
- Colonne: nome, descrizione, parent_id, diocesi_id, indirizzo_incontro, cap_incontro, città_incontro, sigla_provincia_incontro - Colonne: nome, descrizione, parent_id, diocesi_id, indirizzo_incontro, cap_incontro, città_incontro, sigla_provincia_incontro
- Fault-tolerant: skip righe vuote, log errori - Fault-tolerant: skip righe vuote, log errori, try/catch su ogni riga
### 3. ICS Import Eventi ### ICS Import Eventi
- Usa `Sabre\VObject\Reader::read()` già in vendor/ - Usa `Sabre\VObject\Reader::read()` già in vendor/
- Mapping: SUMMARY→nome_evento, DESCRIPTION→descrizione, DTSTART→data_specifica+ora_inizio, DTEND→durata_minuti, LOCATION→luogo_indirizzo, UID→uid_esterno - Mapping: SUMMARY→nome_evento, DESCRIPTION→descrizione, DTSTART→data_specifica+ora_inizio, DTEND→durata_minuti, LOCATION→luogo_indirizzo, UID→uid_esterno
- Dedup via uid_esterno - Dedup via uid_esterno
- RRULE: import come evento singolo con prima occorrenza - RRULE: import come evento singolo con prima occorrenza
### 4. Script build-dist.sh ### build-dist.sh (script di distribuzione)
- Genera `glastree-YYYYMMDD_HHMM.tar.gz` (~26MB) - Genera `glastree-YYYYMMDD_HHMM.tar.gz`
- Include: sorgenti, vendor (production), Dockerfile, docker-compose, install.php, install.sql - Include: sorgenti, vendor (production)
- Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests - Esclude: .git, node_modules, tests, cache, storage content, vendor/docs/tests, Docker
- **Istruzioni post-estrazione complete**: mkdir, permessi, configurazione, cache clear
## Installazione su server remoto ## Bug Fix Recenti
### 2026-06-07 — Colonne mancanti in eventi e gruppo_individuo
- `descrizione_evento` e `is_incontro_gruppo` mancanti in install.sql → creata migration + ALTER TABLE
- `ruolo_nel_gruppo` mancante in install.sql per pivot `gruppo_individuo`
- `VistaReport.php`: aggiunto `is_default` a `$fillable`
### 2026-06-08 — build-dist.sh: "provide valid cache path" su server remoto
**Problema**: Le esclusioni in `build-dist.sh` usavano `--exclude='storage/framework/cache'` (e simili per sessions, views, logs), che escludevano **l'intera directory** dall'archivio. Dopo l'estrazione, le directory necessarie a Laravel non esistevano.
**Fix**:
1. Cambiati exclude patterns per **contenuti soltanto** (non la directory):
- `--exclude='storage/framework/cache/*'` (non `/cache`)
- `--exclude='storage/framework/sessions/*'`
- `--exclude='storage/framework/views/*'`
- `--exclude='storage/logs/*'`
- `--exclude='storage/debugbar/*'`
2. Aggiunte esclusioni per upload utente:
- `--exclude='storage/app/public'`
- `--exclude='storage/app/private'`
3. Creato `.gitignore` in `storage/framework/views/` (mancante)
4. Istruzioni post-estrazione riscritte con:
- `mkdir -p` per tutte le directory necessarie (cache, sessions, views, logs, public, backups, documenti, bootstrap/cache)
- `chmod -R 775` e `chown -R www-data:www-data` per permessi
- `php artisan config:clear`, `route:clear`, `view:clear`
## Problema 419 (Page Expired) su Produzione
Il codice NON ha errori di CSRF — tutte le form hanno `@csrf`. Il problema è configurazione sessione del server:
1. `storage/framework/sessions/` non scrivibile dal web server
2. `APP_URL` in `.env` non corrisponde all'URL reale
3. `SESSION_SECURE_COOKIE=true` ma sito in HTTP
Soluzione:
```bash ```bash
# Estrarre chmod -R 775 storage/framework/sessions
tar xzf glastree-20260607_2052.tar.gz php artisan config:clear
# Configurare
cp .env.example .env
php artisan key:generate
php artisan storage:link
# Opzione A — Docker
docker compose up -d
# Opzione B — Apache nativo (MySQL)
php install.php
``` ```
### 2026-06-08 — build-dist.sh: impossibilità salvare logo su server remoto
**Problema**:
- `public/storage` era un symlink **assoluto** (`/var/www/html/glastree/storage/app/public`) incluso nell'archivio → all'estrazione su altro server puntava a path inesistente.
- `php artisan storage:link` falliva perché il symlink rotto esisteva già.
- Sottodirectory `logos/`, `documenti/eventi/`, `avatars/gruppi/` non create dal `mkdir -p`.
**Fix**:
1. Aggiunto `--exclude='public/storage'` al tar — il symlink va ricreato sul target.
2. Aggiunto `rm -f public/storage` prima di `php artisan storage:link` per evitare conflitto.
3. Usato `ln -sf ../storage/app/public public/storage` (invece di `php artisan storage:link --relative` che richiede `symfony/filesystem` non sempre presente).
4. Aggiunte al `mkdir -p` le sottodirectory mancanti:
- `storage/app/public/logos` (logo upload)
- `storage/app/public/documenti/eventi` (event documenti)
- `storage/app/private/avatars/gruppi` (avatar gruppi)
5. Aggiunta creazione `.gitignore` in `storage/app/public/` (placeholder Laravel).
### Fix form annidato salvataggio email
**2026-06-08 — Problema**: Il form di DELETE era **annidato dentro** il form di SAVE in entrambe le pagine:
- `resources/views/impostazioni/index.blade.php`
- `resources/views/admin/email-settings/index.blade.php`
- HTML invalido: il browser chiudeva implicitamente il form SAVE all'apertura del form DELETE, causando l'invio alla route sbagliata.
**Fix**: Spostato il form DELETE fuori dal form SAVE, separato in un `div` autonomo.
### Fix campi mancanti in validazione EmailSettingsController@save
**2026-06-08**: `signature`, `signature_enabled`, `smtp_username`, `smtp_password` non erano nelle regole di validazione di `save()`. Laravel scarta i campi non validati, quindi non venivano mai scritti nel DB.
**Fix**: Aggiunte le regole mancanti a `$request->validate()`.
### DiagnoseEmail Command
- `php artisan diagnose:email` — controlla APP_KEY, tabella email_settings, record, decrittazione password, sender_accounts, log errori, estensioni PHP.
### 2026-06-08 — Firma email ancora non salvata (secondo tentativo)
**Problema**: La firma (campo `signature`) non veniva salvata nonostante le regole di validazione fossero state aggiunte.
**Root cause analysis**:
1. HTML checkbox `signature_enabled` senza hidden fallback: quando non spuntato, il campo non veniva inviato → non presente in `$validated``updateOrCreate` non lo aggiornava mai a `false`.
2. La vecchia pagina `/impostazioni/` aveva l'handler `shown.bs.tab` per il tab Firma (`#email-firma`) in un solo punto su due, causando possibile mancata inizializzazione dell'editor Quill su alcuni flussi di navigazione.
3. La verifica nel controller controllava solo `imap_host` e `email_address` — anche se signature/signature_enabled fallivano, l'utente vedeva "salvato con successo".
**Fix**:
1. `resources/views/admin/email-settings/index.blade.php:268` — Aggiunto `<input type="hidden" name="signature_enabled" value="0">` prima della checkbox
2. `resources/views/impostazioni/index.blade.php:603` — Stesso hidden fallback
3. `resources/views/impostazioni/index.blade.php:1633` — Aggiunto `#email-firma` al primo handler `shown.bs.tab` (il secondo handler esisteva già ma il primo no)
4. `app/Http/Controllers/Admin/EmailSettingsController.php:70-82` — Aggiunto log di warning se signature o signature_enabled non corrispondono dopo il salvataggio
### 2026-06-08 — Sync email: "Nessuna configurazione email attiva" nonostante test IMAP funzioni
**Problema**: Cliccando "Sincronizza Ora" nelle impostazioni email o "Ricevi/Invia" nella pagina email, viene mostrato "Nessuna configurazione email attiva". Il test IMAP funziona correttamente.
**Root cause**: Stesso bug dei checkbox senza hidden fallback. Il campo `is_active` nel tab Sincronizzazione non veniva inviato quando non spuntato. Alla prima configurazione, se l'utente non visitava esplicitamente il tab Sync e attivava lo switch, `is_active` rimaneva `false` (default DB). Il test IMAP usa `EmailSetting::first()` (ignora `is_active`), mentre `syncEmails()` e `syncNow()` usano `EmailSetting::getActive()` che filtra per `is_active = true`.
**Fix**:
1. `resources/views/admin/email-settings/index.blade.php:251` — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
2. `resources/views/impostazioni/index.blade.php:586` — Stesso hidden fallback
3. `EmailSetting::getActive()` ora riceve sempre `is_active` dal form (0 o 1) grazie all'hidden fallback
## 2026-06-08 — Help page: aggiunto tab "Calendario" con documentazione OAuth
- **`resources/views/help/index.blade.php`**: Aggiunto tab "Calendario" nel menu laterale (dopo Google Drive) e relativo tab-pane content con:
- Requisiti (account Google, API Calendar, OAuth credentials)
- Passo 1: Google Cloud Console — abilitare Google Calendar API, creare OAuth Client ID (Web Application), redirect URI `https://developers.google.com/oauthplayground`
- Passo 2: Ottenere Refresh Token via Google OAuth Playground (Use your own OAuth credentials, scope `calendar`)
- Passo 3: Configurazione nell'app (Impostazioni → Calendario, nuova connessione Google Calendar, inserire Client ID/Secret/Refresh Token)
- Test connessione e sincronizzazione
- Nota: refresh token permanente finché non revocato
- **`resources/views/help/pdf.blade.php`**: Aggiunta sezione "3. Google Calendar" nel TOC e contenuto, rinumerazione da 4 a 8 per sezioni successive
### Differenza chiave Google Calendar vs Google Drive
- **Drive**: endpoint server `/auth/google-drive/callback` che gestisce automaticamente il callback OAuth e salva refresh token in `storage_repositories`
- **Calendar**: **NON** ha endpoint server dedicato. Refresh token ottenuto manualmente via Google OAuth Playground e incollato nel campo "Refresh Token" del form
## 2026-06-08 — Pulsante Sync nella pagina calendario eventi
- **`app/Http/Controllers/CalendarioConnessioneController.php`**: Aggiunto metodo `syncAll()` che itera tutte le connessioni attive (`is_active = true`), esegue `syncEvents()` per ognuna, e restituisce JSON con risultati aggregati.
- **`routes/web.php`**: Aggiunta route `POST /calendario-connessioni/sync-all``syncAll` (name: `calendario-connessioni.sync-all`).
- **`resources/views/eventi/calendar.blade.php`**: Aggiunto pulsante "Sync" nel card-tools che:
- Invia POST AJAX a `sync-all`
- Mostra spinner durante l'operazione
- Al completamento mostra alert con riepilogo (connessioni, importati, esportati)
- Richiama `window.calendar.refetchEvents()` per aggiornare la vista FullCalendar
- Esposto `window.calendar` globalmente per accesso dall'handler sync
## 2026-06-08 — Fix logo non visibile su installazione remota
**Problema**: Il logo uploadato non veniva visualizzato su installazioni remote, pur salvandosi correttamente su disco.
**Root cause**: `getLogoPath()` e `getLogoSmallPath()` in `AppSetting.php` usavano `file_exists(public_path('storage/' . $path))` per verificare l'esistenza del file prima di restituire l'URL. Questo controllo dipendeva dal symlink `public/storage → storage/app/public`. Su installazioni remote dove il symlink era assente o mal configurato, `file_exists` restituiva `false` → metodo restituiva `null` → logo non mostrato.
**Fix**:
1. **`app/Models/AppSetting.php`**: Sostituito `file_exists(public_path('storage/' . $path))` con `Storage::disk('public')->exists($path)` e `asset('storage/' . $path)` con `Storage::disk('public')->url($path)`. Il controllo ora avviene direttamente su `storage/app/public/` (disk root), bypassando il symlink.
2. **`app/Http/Controllers/ImpostazioniController.php`**: In `uploadLogo()`, aggiunta creazione automatica del symlink `public/storage → storage/app/public` se mancante, per garantire che il browser possa servire l'immagine.
## Prossimi Passi
- *(nessuno — in attesa di nuove richieste)*
+167
View File
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\EmailSetting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
class DiagnoseEmail extends Command
{
protected $signature = 'diagnose:email';
protected $description = 'Diagnostica configurazione email e credenziali';
public function handle(): int
{
$this->info('=== Diagnostica Email Settings ===');
$this->newLine();
// 1. APP_KEY
$this->section('APP_KEY');
$appKey = config('app.key');
if ($appKey && str_starts_with($appKey, 'base64:')) {
$this->line('APP_KEY: presente e valida ✓');
} elseif ($appKey) {
$this->warn('APP_KEY: presente ma non in formato base64 (potrebbe funzionare lo stesso)');
} else {
$this->error('APP_KEY: NON IMPOSTATA — crittografia password non funzionerà!');
}
// 2. Tabella email_settings
$this->newLine();
$this->section('Tabella email_settings');
if (Schema::hasTable('email_settings')) {
$this->line('Tabella email_settings: esiste ✓');
$columns = Schema::getColumnListing('email_settings');
$this->line('Colonne: ' . implode(', ', $columns));
$required = ['imap_host', 'imap_username', 'imap_password', 'email_address'];
$missing = array_diff($required, $columns);
if ($missing) {
$this->error('Colonne mancanti: ' . implode(', ', $missing));
} else {
$this->line('Colonne richieste presenti ✓');
}
} else {
$this->error('Tabella email_settings NON ESISTE! Esegui: php artisan migrate');
}
// 3. Record email_settings
$this->newLine();
$this->section('Record email_settings');
$settings = EmailSetting::first();
if ($settings) {
$this->line('Record trovato (ID: ' . $settings->id . ')');
$this->line(' imap_host: ' . ($settings->imap_host ?: 'VUOTO'));
$this->line(' imap_username: ' . ($settings->imap_username ?: 'VUOTO'));
$this->line(' imap_password: ' . ($settings->imap_password ? '[CRITTATO]' : 'VUOTO'));
$this->line(' email_address: ' . ($settings->email_address ?: 'VUOTO'));
$this->line(' smtp_host: ' . ($settings->smtp_host ?: 'VUOTO'));
// Prova decrittazione
if ($settings->imap_password) {
try {
$decrypted = Crypt::decryptString($settings->imap_password);
$this->line(' -> password IMAP decrittata: ' . (strlen($decrypted) > 0 ? 'OK (' . strlen($decrypted) . ' caratteri)' : 'VUOTA'));
} catch (\Exception $e) {
$this->error(' -> ERRORE decrittazione password IMAP: ' . $e->getMessage());
$this->warn(' Causa probabile: APP_KEY diversa da quando è stata salvata.');
}
}
} else {
$this->warn('NESSUN record in email_settings — nessuna configurazione salvata.');
$this->line('Vai su /impostazioni#email-imap, compila e clicca "Salva Impostazioni Email"');
}
// 4. Tabella sender_accounts
$this->newLine();
$this->section('Tabella sender_accounts');
if (Schema::hasTable('sender_accounts')) {
$this->line('Tabella sender_accounts: esiste ✓');
$count = DB::table('sender_accounts')->count();
$this->line('Record presenti: ' . $count);
} else {
$this->error('Tabella sender_accounts NON ESISTE!');
}
// 5. Log recenti
$this->newLine();
$this->section('Errori recenti (storage/logs/laravel.log)');
$logPath = storage_path('logs/laravel.log');
if (file_exists($logPath)) {
$lines = $this->getLastErrorLines($logPath, 20);
if ($lines) {
foreach ($lines as $line) {
$this->line($line);
}
} else {
$this->line('Nessun errore recente trovato.');
}
} else {
$this->warn('File di log non trovato: ' . $logPath);
}
// 6. Estensioni PHP
$this->newLine();
$this->section('Estensioni PHP');
$requiredExts = ['openssl', 'json', 'mbstring', 'pdo', 'fileinfo'];
foreach ($requiredExts as $ext) {
if (extension_loaded($ext)) {
$this->line($ext . ': OK ✓');
} else {
$this->error($ext . ': MANCANTE!');
}
}
$this->newLine();
$this->info('=== Diagnostica completata ===');
return self::SUCCESS;
}
private function section(string $title): void
{
$this->line("--- {$title} ---");
}
private function getLastErrorLines(string $path, int $maxLines): array
{
$lines = [];
$fp = fopen($path, 'r');
if (!$fp) {
return $lines;
}
fseek($fp, -1, SEEK_END);
$pos = ftell($fp);
$buffer = '';
while ($pos > 0 && count($lines) < $maxLines) {
$char = fgetc($fp);
if ($char === "\n") {
if (trim($buffer) !== '') {
$lines[] = trim($buffer);
}
$buffer = '';
} else {
$buffer = $char . $buffer;
}
$pos--;
fseek($fp, $pos);
}
if (trim($buffer) !== '') {
$lines[] = trim($buffer);
}
fclose($fp);
return $lines;
}
}
@@ -34,11 +34,15 @@ class EmailSettingsController extends Controller
'smtp_host' => 'nullable|string|max:255', 'smtp_host' => 'nullable|string|max:255',
'smtp_port' => 'nullable|integer|min:1|max:65535', 'smtp_port' => 'nullable|integer|min:1|max:65535',
'smtp_encryption' => 'nullable|in:ssl,tls,none', 'smtp_encryption' => 'nullable|in:ssl,tls,none',
'smtp_username' => 'nullable|string|max:255',
'smtp_password' => 'nullable|string',
'email_address' => 'required|email', 'email_address' => 'required|email',
'email_name' => 'nullable|string|max:255', 'email_name' => 'nullable|string|max:255',
'reply_to' => 'nullable|email', 'reply_to' => 'nullable|email',
'sync_interval_minutes' => 'nullable|integer|min:1|max:60', 'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
'is_active' => 'boolean', 'is_active' => 'boolean',
'signature' => 'nullable|string',
'signature_enabled' => 'boolean',
]); ]);
if (!empty($validated['imap_password'])) { if (!empty($validated['imap_password'])) {
@@ -62,6 +66,24 @@ class EmailSettingsController extends Controller
if ($verify && if ($verify &&
$verify->imap_host === $validated['imap_host'] && $verify->imap_host === $validated['imap_host'] &&
$verify->email_address === $validated['email_address']) { $verify->email_address === $validated['email_address']) {
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature'];
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled'];
if (!$signatureMatch) {
\Illuminate\Support\Facades\Log::warning('Signature mismatch after save', [
'expected' => $validated['signature'] ?? null,
'actual' => $verify->signature,
]);
}
if (!$signatureEnabledMatch) {
\Illuminate\Support\Facades\Log::warning('Signature enabled mismatch after save', [
'expected' => $validated['signature_enabled'] ?? null,
'actual' => $verify->signature_enabled,
]);
}
return back()->with('success', 'Impostazioni email salvate e verificate nel database.'); return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
} else { } else {
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.'); return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
@@ -126,6 +126,52 @@ class CalendarioConnessioneController extends Controller
return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg); return redirect('/impostazioni#calendario')->with('error', 'Errore sync: ' . $errorMsg);
} }
public function syncAll(): JsonResponse
{
$this->authorizeWrite('settings');
$connections = CalendarioConnessione::where('is_active', true)->get();
$results = [];
foreach ($connections as $connessione) {
$result = match ($connessione->tipo) {
'caldav' => app(CalDavService::class)->syncEvents($connessione),
'google_calendar' => app(GoogleCalendarSyncService::class)->syncEvents($connessione),
default => ['success' => false, 'imported' => 0, 'exported' => 0, 'errors' => ['Tipo sconosciuto']],
};
$connessione->update(['last_sync_at' => now()]);
if ($result['success']) {
$connessione->update(['stato' => 'connesso']);
} else {
$errorMsg = implode('; ', $result['errors']);
$connessione->update(['stato' => 'errore', 'last_error_at' => now(), 'last_error_message' => $errorMsg]);
}
$results[] = [
'id' => $connessione->id,
'nome' => $connessione->nome,
'success' => $result['success'],
'imported' => $result['imported'],
'exported' => $result['exported'],
'errors' => $result['errors'],
];
}
$totalImported = array_sum(array_column($results, 'imported'));
$totalExported = array_sum(array_column($results, 'exported'));
$hasErrors = in_array(false, array_column($results, 'success'), true);
return response()->json([
'success' => !$hasErrors,
'results' => $results,
'total_imported' => $totalImported,
'total_exported' => $totalExported,
'connections' => count($results),
]);
}
public function reorder(Request $request): JsonResponse public function reorder(Request $request): JsonResponse
{ {
$this->authorizeWrite('settings'); $this->authorizeWrite('settings');
@@ -398,6 +398,15 @@ class ImpostazioniController extends Controller
$settings->save(); $settings->save();
} }
$linkPath = public_path('storage');
if (!file_exists($linkPath) && !is_link($linkPath)) {
try {
symlink(storage_path('app/public'), $linkPath);
} catch (\Throwable $e) {
Log::warning('Impossibile creare symlink storage: ' . $e->getMessage());
}
}
return redirect('/impostazioni#logo')->with('success', 'Logo caricato con successo.'); return redirect('/impostazioni#logo')->with('success', 'Logo caricato con successo.');
} }
+14 -12
View File
@@ -5,7 +5,9 @@ declare(strict_types=1);
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
class AppSetting extends Model class AppSetting extends Model
{ {
@@ -107,38 +109,38 @@ class AppSetting extends Model
public function getLogoUrlInstance(): ?string public function getLogoUrlInstance(): ?string
{ {
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) { if ($this->logo && Storage::disk('public')->exists($this->logo)) {
return asset('storage/' . $this->logo); return Storage::disk('public')->url($this->logo);
} }
return null; return null;
} }
public function getLogoSmallUrlInstance(): ?string public function getLogoSmallUrlInstance(): ?string
{ {
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) { if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
return asset('storage/' . $this->logo_small); return Storage::disk('public')->url($this->logo_small);
} }
return null; return null;
} }
public function getLogoPath(): ?string public function getLogoPath(): ?string
{ {
if ($this->logo_path && file_exists(public_path('storage/' . $this->logo_path))) { if ($this->logo_path && Storage::disk('public')->exists($this->logo_path)) {
return asset('storage/' . $this->logo_path); return Storage::disk('public')->url($this->logo_path);
} }
if ($this->logo && file_exists(public_path('storage/' . $this->logo))) { if ($this->logo && Storage::disk('public')->exists($this->logo)) {
return asset('storage/' . $this->logo); return Storage::disk('public')->url($this->logo);
} }
return null; return null;
} }
public function getLogoSmallPath(): ?string public function getLogoSmallPath(): ?string
{ {
if ($this->logo_small_path && file_exists(public_path('storage/' . $this->logo_small_path))) { if ($this->logo_small_path && Storage::disk('public')->exists($this->logo_small_path)) {
return asset('storage/' . $this->logo_small_path); return Storage::disk('public')->url($this->logo_small_path);
} }
if ($this->logo_small && file_exists(public_path('storage/' . $this->logo_small))) { if ($this->logo_small && Storage::disk('public')->exists($this->logo_small)) {
return asset('storage/' . $this->logo_small); return Storage::disk('public')->url($this->logo_small);
} }
return null; return null;
} }
+31 -8
View File
@@ -37,11 +37,14 @@ tar czf "${ARCHIVE}" \
--exclude='vendor/*/doc' \ --exclude='vendor/*/doc' \
--exclude='storage/app/backups' \ --exclude='storage/app/backups' \
--exclude='storage/app/documenti' \ --exclude='storage/app/documenti' \
--exclude='storage/framework/cache' \ --exclude='storage/app/public' \
--exclude='storage/framework/sessions' \ --exclude='storage/app/private' \
--exclude='storage/framework/views' \ --exclude='public/storage' \
--exclude='storage/logs' \ --exclude='storage/framework/cache/*' \
--exclude='storage/debugbar' \ --exclude='storage/framework/sessions/*' \
--exclude='storage/framework/views/*' \
--exclude='storage/logs/*' \
--exclude='storage/debugbar/*' \
--exclude='bootstrap/cache/*.php' \ --exclude='bootstrap/cache/*.php' \
--exclude='public/hot' \ --exclude='public/hot' \
--exclude='*.tar.gz' \ --exclude='*.tar.gz' \
@@ -67,7 +70,27 @@ echo "Per estrarre sul server di destinazione:"
echo " tar xzf ${ARCHIVE}" echo " tar xzf ${ARCHIVE}"
echo "" echo ""
echo "Poi esegui:" echo "Poi esegui:"
echo " cp .env.example .env # configura il tuo ambiente" echo " # 1. Crea directory necessarie (se non esistono già)"
echo ' mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache storage/app/public storage/app/public/logos storage/app/public/documenti/eventi storage/app/backups storage/app/documenti storage/app/private/avatars/gruppi'
echo ""
echo " # 2. Crea file .gitignore in storage/app/public (serve a Laravel)"
echo ' echo "*" > storage/app/public/.gitignore'
echo ' echo "!.gitignore" >> storage/app/public/.gitignore'
echo ""
echo " # 3. Permessi (web server = www-data)"
echo ' chmod -R 775 storage bootstrap/cache'
echo ' chown -R www-data:www-data storage bootstrap/cache'
echo ""
echo " # 4. Configura ambiente"
echo " cp .env.example .env # modifica DB, APP_URL, etc."
echo " php artisan key:generate" echo " php artisan key:generate"
echo " php artisan storage:link" echo ""
echo " php install.php # avvia installazione MySQL" echo " # 5. Pulisci cache e ricrea symlink storage (relativo per portabilità)"
echo ' rm -f public/storage'
echo ' ln -sf ../storage/app/public public/storage'
echo " php artisan config:clear"
echo " php artisan route:clear"
echo " php artisan view:clear"
echo ""
echo " # 6. Avvia installazione MySQL"
echo " php install.php"
@@ -248,6 +248,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" class="custom-control-input" id="is_active" <input type="checkbox" class="custom-control-input" id="is_active"
name="is_active" value="1" {{ ($settings->is_active ?? false) ? 'checked' : '' }}> name="is_active" value="1" {{ ($settings->is_active ?? false) ? 'checked' : '' }}>
<label class="custom-control-label" for="is_active">Account Email Attivo</label> <label class="custom-control-label" for="is_active">Account Email Attivo</label>
@@ -265,6 +266,7 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="hidden" name="signature_enabled" value="0">
<input type="checkbox" class="custom-control-input" id="signature_enabled" <input type="checkbox" class="custom-control-input" id="signature_enabled"
name="signature_enabled" value="1" {{ ($settings->signature_enabled ?? true) ? 'checked' : '' }}> name="signature_enabled" value="1" {{ ($settings->signature_enabled ?? true) ? 'checked' : '' }}>
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label> <label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
@@ -390,14 +392,17 @@
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2"> <a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
<i class="fas fa-sync mr-1"></i> Sincronizza Ora <i class="fas fa-sync mr-1"></i> Sincronizza Ora
</a> </a>
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')"> </div>
</form>
<div class="d-flex justify-content-end mt-2">
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
@csrf @method('DELETE') @csrf @method('DELETE')
<button type="submit" class="btn btn-danger"> <button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash mr-1"></i> Elimina Configurazione <i class="fas fa-trash mr-1"></i> Elimina Configurazione
</button> </button>
</form> </form>
</div> </div>
</form>
</div> </div>
</div> </div>
@endsection @endsection
+39 -2
View File
@@ -17,6 +17,9 @@
<a href="{{ route('eventi.create') }}" class="btn btn-success btn-sm"> <a href="{{ route('eventi.create') }}" class="btn btn-success btn-sm">
<i class="fas fa-plus mr-1"></i> Nuovo Evento <i class="fas fa-plus mr-1"></i> Nuovo Evento
</a> </a>
<button type="button" class="btn btn-info btn-sm" id="syncCalendarsBtn" onclick="syncCalendars()">
<i class="fas fa-sync mr-1"></i> Sync
</button>
<form action="{{ route('eventi.export-selected-ics') }}" method="POST" class="d-inline"> <form action="{{ route('eventi.export-selected-ics') }}" method="POST" class="d-inline">
@csrf @csrf
<button type="submit" class="btn btn-success btn-sm"> <button type="submit" class="btn btn-success btn-sm">
@@ -40,10 +43,44 @@
@section('scripts') @section('scripts')
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
<script> <script>
function syncCalendars() {
var btn = document.getElementById('syncCalendarsBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Sync...';
fetch('{{ route('calendario-connessioni.sync-all') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
'Content-Type': 'application/json',
},
})
.then(function(r) { return r.json(); })
.then(function(data) {
var msg = data.connections + ' connessioni sincronizzate.';
msg += ' Importati: ' + data.total_imported + ', Esportati: ' + data.total_exported + '.';
if (data.success) {
alert('✅ Sync completato!\n' + msg);
} else {
alert('⚠️ Sync completato con errori.\n' + msg);
}
if (window.calendar) {
window.calendar.refetchEvents();
}
})
.catch(function(err) {
alert('❌ Errore sync: ' + err.message);
})
.finally(function() {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync mr-1"></i> Sync';
});
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar'); var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, { window.calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
locale: 'it', locale: 'it',
headerToolbar: { headerToolbar: {
@@ -97,7 +134,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
calendar.render(); window.calendar.render();
}); });
</script> </script>
@endsection @endsection
+74
View File
@@ -27,6 +27,11 @@
<i class="fab fa-google-drive mr-2"></i> Google Drive <i class="fab fa-google-drive mr-2"></i> Google Drive
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="#help-calendario" data-toggle="tab">
<i class="fas fa-calendar-alt mr-2"></i> Calendario
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#help-tipologie" data-toggle="tab"> <a class="nav-link" href="#help-tipologie" data-toggle="tab">
<i class="fas fa-tags mr-2"></i> Tipologie e Dati <i class="fas fa-tags mr-2"></i> Tipologie e Dati
@@ -194,6 +199,75 @@
</div> </div>
</div> </div>
{{-- Calendario --}}
<div class="tab-pane" id="help-calendario">
<div class="card card-outline card-info">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-calendar-alt mr-2"></i> Google Calendar</h3>
</div>
<div class="card-body">
<h5>Cos'è</h5>
<p>Integrazione con Google Calendar per sincronizzare gli eventi tra l'app e il tuo calendario Google. Supporta import, export e sincronizzazione bidirezionale.</p>
<h5>Requisiti</h5>
<ol>
<li>Un account Google / Google Workspace</li>
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Calendar abilitata</li>
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
</ol>
<h5>Passo 1: Creare il progetto Google Cloud</h5>
<ol>
<li>Vai su <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></li>
<li>Crea un nuovo progetto o selezionane uno esistente</li>
<li>Vai a <strong>API e servizi Libreria</strong></li>
<li>Cerca "<strong>Google Calendar API</strong>" e <strong>abilitala</strong></li>
<li>Vai a <strong>API e servizi Credenziali</strong></li>
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code>https://developers.google.com/oauthplayground</code></li>
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
</ol>
<h5>Passo 2: Ottenere il Refresh Token (Google OAuth Playground)</h5>
<p>A differenza di Google Drive, per Google Calendar il refresh token va ottenuto manualmente tramite il Google OAuth Playground:</p>
<ol>
<li>Vai su <a href="https://developers.google.com/oauthplayground" target="_blank">Google OAuth Playground</a></li>
<li>Clicca l'ingranaggio ⚙️ in alto a destra</li>
<li>Spunta "<strong>Use your own OAuth credentials</strong>"</li>
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong> creati al Passo 1</li>
<li>Nella colonna sinistra, espandi "<strong>Google Calendar API v3</strong>" e seleziona lo scope <code>https://www.googleapis.com/auth/calendar</code></li>
<li>Clicca "<strong>Authorize APIs</strong>" e autorizza con l'account Google</li>
<li>Clicca "<strong>Exchange authorization code for tokens</strong>"</li>
<li><strong>Copia il Refresh Token</strong> generato</li>
</ol>
<h5>Passo 3: Configurazione nell'app</h5>
<ol>
<li>Vai in <strong>Impostazioni → Calendario</strong></li>
<li>Clicca "<strong>Nuova Connessione</strong>"</li>
<li>Inserisci un nome descrittivo (es. "Calendario Lavoro")</li>
<li>Seleziona tipo <strong>Google Calendar</strong></li>
<li>Incolla <strong>Client ID</strong>, <strong>Client Secret</strong> e il <strong>Refresh Token</strong> ottenuto</li>
<li><strong>Calendar ID (opzionale):</strong> lascia <code>primary</code> per il calendario principale, oppure inserisci l'ID di un calendario specifico (lo trovi in Google Calendar Impostazioni calendario ID calendario)</li>
<li>Seleziona la <strong>direzione di sincronizzazione</strong> (import, export o bidirezionale)</li>
<li>Clicca "<strong>Salva</strong>"</li>
</ol>
<h5>Test connessione</h5>
<p>Dopo aver salvato, usa il pulsante <span class="badge badge-success"><i class="fas fa-plug"></i> Test</span> nella tabella delle connessioni per verificare che l'autorizzazione funzioni correttamente.</p>
<h5>Sincronizzazione</h5>
<p>Dopo aver configurato la connessione, puoi sincronizzare gli eventi manualmente cliccando <span class="badge badge-primary"><i class="fas fa-sync"></i> Sync</span>. La sincronizzazione automatica viene eseguita in base all'intervallo impostato (richiede il cron di Laravel configurato).</p>
<div class="alert alert-info">
<i class="fas fa-info-circle mr-2"></i>
Il Refresh Token è <strong>permanente</strong> finché non viene revocato dall'utente. Se la connessione smette di funzionare, potrebbe essere necessario rigenerarlo ripetendo il Passo 2.
</div>
</div>
</div>
</div>
{{-- Tipologie e Dati --}} {{-- Tipologie e Dati --}}
<div class="tab-pane" id="help-tipologie"> <div class="tab-pane" id="help-tipologie">
<div class="card card-outline card-success"> <div class="card card-outline card-success">
+75 -10
View File
@@ -62,6 +62,7 @@
<ol> <ol>
<li><a href="#webdav">WebDAV / Nextcloud</a></li> <li><a href="#webdav">WebDAV / Nextcloud</a></li>
<li><a href="#googledrive">Google Drive</a></li> <li><a href="#googledrive">Google Drive</a></li>
<li><a href="#calendario">Google Calendar</a></li>
<li><a href="#tipologie">Tipologie e Gestione Dati</a></li> <li><a href="#tipologie">Tipologie e Gestione Dati</a></li>
<li><a href="#email">Email / SMTP</a></li> <li><a href="#email">Email / SMTP</a></li>
<li><a href="#installazione">Guida all'Installazione</a></li> <li><a href="#installazione">Guida all'Installazione</a></li>
@@ -182,8 +183,72 @@
<div class="page-break"></div> <div class="page-break"></div>
{{-- 3. Tipologie e Dati --}} {{-- 3. Google Calendar --}}
<h2 id="tipologie">3. Tipologie e Gestione Dati</h2> <h2 id="calendario">3. Google Calendar</h2>
<h3>Cos'è</h3>
<p>Integrazione con Google Calendar per sincronizzare gli eventi tra l'app e il tuo calendario Google. Supporta import, export e sincronizzazione bidirezionale.</p>
<h3>Requisiti</h3>
<ol>
<li>Un account Google / Google Workspace</li>
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Calendar abilitata</li>
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
</ol>
<h3>Passo 1: Creare il progetto Google Cloud</h3>
<ol>
<li>Vai su <strong>Google Cloud Console</strong> (console.developers.google.com)</li>
<li>Crea un nuovo progetto o selezionane uno esistente</li>
<li>Vai a <strong>API e servizi → Libreria</strong></li>
<li>Cerca "<strong>Google Calendar API</strong>" e <strong>abilitala</strong></li>
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code>https://developers.google.com/oauthplayground</code></li>
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
</ol>
<h3>Passo 2: Ottenere il Refresh Token</h3>
<p>A differenza di Google Drive, per Google Calendar il refresh token va ottenuto manualmente tramite il Google OAuth Playground:</p>
<ol>
<li>Vai su <strong>Google OAuth Playground</strong> (developers.google.com/oauthplayground)</li>
<li>Clicca l'ingranaggio ⚙️ in alto a destra</li>
<li>Spunta "<strong>Use your own OAuth credentials</strong>"</li>
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong> creati al Passo 1</li>
<li>Nella colonna sinistra, espandi "<strong>Google Calendar API v3</strong>" e seleziona lo scope <code>https://www.googleapis.com/auth/calendar</code></li>
<li>Clicca "<strong>Authorize APIs</strong>" e autorizza con l'account Google</li>
<li>Clicca "<strong>Exchange authorization code for tokens</strong>"</li>
<li><strong>Copia il Refresh Token</strong> generato</li>
</ol>
<h3>Passo 3: Configurazione nell'app</h3>
<ol>
<li>Vai in <strong>Impostazioni Calendario</strong></li>
<li>Clicca "<strong>Nuova Connessione</strong>"</li>
<li>Inserisci un nome descrittivo (es. "Calendario Lavoro")</li>
<li>Seleziona tipo <strong>Google Calendar</strong></li>
<li>Incolla <strong>Client ID</strong>, <strong>Client Secret</strong> e il <strong>Refresh Token</strong> ottenuto</li>
<li><strong>Calendar ID (opzionale):</strong> lascia <code>primary</code> per il calendario principale, oppure inserisci l'ID di un calendario specifico</li>
<li>Seleziona la <strong>direzione di sincronizzazione</strong> (import, export o bidirezionale)</li>
<li>Clicca "<strong>Salva</strong>"</li>
</ol>
<h3>Test connessione</h3>
<p>Dopo aver salvato, usa il pulsante <strong>Test</strong> nella tabella delle connessioni per verificare che l'autorizzazione funzioni correttamente.</p>
<h3>Sincronizzazione</h3>
<p>Dopo aver configurato la connessione, puoi sincronizzare gli eventi manualmente cliccando <strong>Sync</strong>. La sincronizzazione automatica viene eseguita in base all'intervallo impostato (richiede il cron di Laravel configurato).</p>
<div class="callout callout-info">
<h4>Refresh Token permanente</h4>
<p>Il Refresh Token è permanente finché non viene revocato dall'utente. Se la connessione smette di funzionare, potrebbe essere necessario rigenerarlo ripetendo il Passo 2.</p>
</div>
<div class="page-break"></div>
{{-- 4. Tipologie e Dati --}}
<h2 id="tipologie">4. Tipologie e Gestione Dati</h2>
<h3>Tipologie Documenti</h3> <h3>Tipologie Documenti</h3>
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p> <p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
@@ -210,8 +275,8 @@
<div class="page-break"></div> <div class="page-break"></div>
{{-- 4. Email / SMTP --}} {{-- 5. Email / SMTP --}}
<h2 id="email">4. Email / SMTP</h2> <h2 id="email">5. Email / SMTP</h2>
<h3>Configurazione Email</h3> <h3>Configurazione Email</h3>
<p>Vai in <strong>Impostazioni Email</strong> per configurare la posta elettronica.</p> <p>Vai in <strong>Impostazioni Email</strong> per configurare la posta elettronica.</p>
@@ -264,8 +329,8 @@
<div class="page-break"></div> <div class="page-break"></div>
{{-- 5. Installazione --}} {{-- 6. Installazione --}}
<h2 id="installazione">5. Guida all'Installazione</h2> <h2 id="installazione">6. Guida all'Installazione</h2>
<h3>Panoramica</h3> <h3>Panoramica</h3>
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p> <p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalità:</p>
@@ -411,8 +476,8 @@ sudo systemctl reload apache2</pre>
<div class="page-break"></div> <div class="page-break"></div>
{{-- 6. Backup e Migrazione --}} {{-- 7. Backup e Migrazione --}}
<h2 id="backup">6. Backup e Migrazione Server</h2> <h2 id="backup">7. Backup e Migrazione Server</h2>
<h3>Cos'è</h3> <h3>Cos'è</h3>
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p> <p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
@@ -591,8 +656,8 @@ sudo certbot --apache -d glastree.esempio.it</pre>
<div class="page-break"></div> <div class="page-break"></div>
{{-- 7. Docker --}} {{-- 8. Docker --}}
<h2 id="docker">7. Docker</h2> <h2 id="docker">8. Docker</h2>
<h3>Creare un'immagine Docker con dati di base</h3> <h3>Creare un'immagine Docker con dati di base</h3>
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p> <p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
+11 -3
View File
@@ -583,6 +583,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" class="custom-control-input" id="is_active" <input type="checkbox" class="custom-control-input" id="is_active"
name="is_active" value="1" {{ ($emailSettings->is_active ?? false) ? 'checked' : '' }}> name="is_active" value="1" {{ ($emailSettings->is_active ?? false) ? 'checked' : '' }}>
<label class="custom-control-label" for="is_active">Account Email Attivo</label> <label class="custom-control-label" for="is_active">Account Email Attivo</label>
@@ -600,6 +601,7 @@
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="hidden" name="signature_enabled" value="0">
<input type="checkbox" class="custom-control-input" id="signature_enabled" <input type="checkbox" class="custom-control-input" id="signature_enabled"
name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}> name="signature_enabled" value="1" {{ ($emailSettings->signature_enabled ?? true) ? 'checked' : '' }}>
<label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label> <label class="custom-control-label" for="signature_enabled">Attiva firma automatica</label>
@@ -722,14 +724,17 @@
<a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2"> <a href="{{ route('impostazioni.email.sync') }}" class="btn btn-warning ml-2">
<i class="fas fa-sync mr-1"></i> Sincronizza Ora <i class="fas fa-sync mr-1"></i> Sincronizza Ora
</a> </a>
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" class="ml-auto" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')"> </div>
</form>
<div class="d-flex justify-content-end mt-2">
<form action="{{ route('impostazioni.email.destroy') }}" method="POST" onsubmit="return confirm('Eliminare definitivamente la configurazione email? Tutti i dati di connessione verranno rimossi.')">
@csrf @method('DELETE') @csrf @method('DELETE')
<button type="submit" class="btn btn-danger"> <button type="submit" class="btn btn-danger btn-sm">
<i class="fas fa-trash mr-1"></i> Elimina Configurazione <i class="fas fa-trash mr-1"></i> Elimina Configurazione
</button> </button>
</form> </form>
</div> </div>
</form>
</div> </div>
{{-- === CALENDARIO === --}} {{-- === CALENDARIO === --}}
@@ -1626,6 +1631,9 @@ $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
sortableCalendario.refresh(); sortableCalendario.refresh();
} }
} }
if (target === '#email-firma') {
setTimeout(initSignatureEditor, 100);
}
}); });
// ===== MITTENTI AGGIUNTIVI ===== // ===== MITTENTI AGGIUNTIVI =====
+1
View File
@@ -203,6 +203,7 @@ Route::put('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConne
Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy'); Route::delete('/calendario-connessioni/{id}', [App\Http\Controllers\CalendarioConnessioneController::class, 'destroy'])->middleware('auth')->name('calendario-connessioni.destroy');
Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test'); Route::post('/calendario-connessioni/{id}/test', [App\Http\Controllers\CalendarioConnessioneController::class, 'test'])->middleware('auth')->name('calendario-connessioni.test');
Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync'); Route::post('/calendario-connessioni/{id}/sync', [App\Http\Controllers\CalendarioConnessioneController::class, 'sync'])->middleware('auth')->name('calendario-connessioni.sync');
Route::post('/calendario-connessioni/sync-all', [App\Http\Controllers\CalendarioConnessioneController::class, 'syncAll'])->middleware('auth')->name('calendario-connessioni.sync-all');
Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder'); Route::post('/calendario-connessioni/reorder', [App\Http\Controllers\CalendarioConnessioneController::class, 'reorder'])->middleware('auth')->name('calendario-connessioni.reorder');
// Storage Repositories (Opzione B) // Storage Repositories (Opzione B)
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
@@ -1,962 +0,0 @@
<?php $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree'; ?>
<?php $__env->startSection('title', 'Guida - ' . $appName); ?>
<?php $__env->startSection('page_title', 'Guida alla Configurazione'); ?>
<?php $__env->startSection('breadcrumbs'); ?>
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item active">Guida</li>
<?php $__env->stopSection(); ?>
<?php $__env->startSection('content'); ?>
<div class="row">
<div class="col-md-3">
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Argomenti</h3>
</div>
<div class="card-body p-0">
<ul class="nav nav-pills flex-column" id="helpTabs">
<li class="nav-item">
<a class="nav-link active" href="#help-webdav" data-toggle="tab">
<i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-googledrive" data-toggle="tab">
<i class="fab fa-google-drive mr-2"></i> Google Drive
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-email" data-toggle="tab">
<i class="fas fa-envelope mr-2"></i> Email / SMTP
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-installazione" data-toggle="tab">
<i class="fas fa-terminal mr-2"></i> Installazione
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-backup" data-toggle="tab">
<i class="fas fa-hdd mr-2"></i> Backup e Migrazione
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#help-docker" data-toggle="tab">
<i class="fab fa-docker mr-2"></i> Docker
</a>
</li>
</ul>
</div>
<div class="card-footer">
<a href="<?php echo e(route('help.pdf.download')); ?>" class="btn btn-primary btn-block">
<i class="fas fa-file-pdf mr-2"></i> Scarica Guida PDF
</a>
</div>
</div>
</div>
<div class="col-md-9">
<div class="tab-content">
<div class="tab-pane active" id="help-webdav">
<div class="card card-outline card-info">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud</h3>
</div>
<div class="card-body">
<h5>Cos'è</h5>
<p>WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.</p>
<h5>Configurazione</h5>
<p>Vai in <strong>Impostazioni → Repository</strong> e crea un nuovo repository di tipo WebDAV.</p>
<div class="callout callout-info">
<h6>Campi obbligatori</h6>
<dl class="row mb-0">
<dt class="col-sm-3">Nome</dt>
<dd class="col-sm-9">Un nome descrittivo (es. "Nextcloud Lavoro")</dd>
<dt class="col-sm-3">Base URI</dt>
<dd class="col-sm-9">L'URL del tuo server WebDAV.
<br><em>Nextcloud:</em> <code>https://tuodominio.it/remote.php/dav/files/utente/</code>
<br><em>Altri:</em> <code>https://server.webdav.it/</code></dd>
<dt class="col-sm-3">Root</dt>
<dd class="col-sm-9">Percorso base all'interno del server (es. <code>/Documenti/</code>). Lascia <code>/</code> per usare la root.</dd>
<dt class="col-sm-3">Username</dt>
<dd class="col-sm-9">Il tuo nome utente</dd>
<dt class="col-sm-3">Password</dt>
<dd class="col-sm-9">La password dell'account</dd>
<dt class="col-sm-3">Auth Type</dt>
<dd class="col-sm-9"><code>Basic</code> per la maggior parte dei server (Nextcloud, ownCloud).</dd>
</dl>
</div>
<h5 class="mt-4">⚠️ Nome utente con @ (chiocciola)</h5>
<p>Se il tuo nome utente contiene il carattere <code>@</code> (es. <code>mario.rossi@azienda.com</code>), segui queste regole:</p>
<ul>
<li><strong>Nel campo "Base URI":</strong> l'indirizzo email deve essere <strong>URL-encoded</strong>:
<br><code>https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/</code>
<br><small>(il carattere <code>@</code> diventa <code>%40</code>)</small></li>
<li><strong>Nel campo "Username":</strong> inserisci l'indirizzo email <strong>normale</strong> con la <code>@</code> vera.</li>
</ul>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Il sistema gestisce automaticamente la differenza tra l'URI (che richiede <code>%40</code>) e l'autenticazione (che usa la <code>@</code> reale).
</div>
<h5 class="mt-4">Test connessione</h5>
<p>Dopo aver salvato, usa il pulsante <span class="badge badge-success"><i class="fas fa-plug"></i> Test</span> nella tabella dei repository per verificare che la connessione funzioni.</p>
<h5 class="mt-4">Importare file</h5>
<p>Apri la sezione <strong>Documenti</strong>, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span> su un file per importarlo come documento locale.</p>
</div>
</div>
</div>
<div class="tab-pane" id="help-googledrive">
<div class="card card-outline card-danger">
<div class="card-header">
<h3 class="card-title"><i class="fab fa-google-drive mr-2"></i> Google Drive</h3>
</div>
<div class="card-body">
<h5>Cos'è</h5>
<p>Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.</p>
<h5>Requisiti</h5>
<ol>
<li>Un account Google / Google Workspace</li>
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Drive abilitata</li>
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
</ol>
<h5>Passo 1: Creare il progetto Google Cloud</h5>
<ol>
<li>Vai su <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></li>
<li>Crea un nuovo progetto o selezionane uno esistente</li>
<li>Vai a <strong>API e servizi → Libreria</strong></li>
<li>Cerca "<strong>Google Drive API</strong>" e <strong>abilitala</strong></li>
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code><?php echo e(url('/auth/google-drive/callback')); ?></code></li>
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
</ol>
<h5>Passo 2: Configurazione nell'app</h5>
<ol>
<li>Vai in <strong>Impostazioni → Repository</strong></li>
<li>Clicca "<strong>Nuovo Repository</strong>" e seleziona tipo <strong>Google Drive</strong></li>
<li>Inserisci un nome descrittivo (es. "Drive Lavoro")</li>
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong></li>
<li><strong>Folder ID (opzionale):</strong> se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: <code>https://drive.google.com/drive/folders/<strong>ID_QUI</strong></code>)</li>
<li>Clicca "<strong>Autorizza Google Drive</strong>" — verrai reindirizzato a Google per concedere i permessi</li>
<li>Dopo l'autorizzazione, il refresh token verrà salvato automaticamente</li>
</ol>
<h5>⚠️ Google Drive API non abilitata</h5>
<p>Se vedi l'errore <em>"Google Drive API non abilitata"</em> nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.</p>
<div class="callout callout-danger">
<h6>Soluzione</h6>
<p>Vai su <a href="https://console.developers.google.com/apis/api/drive.googleapis.com/overview" target="_blank">
https://console.developers.google.com/apis/api/drive.googleapis.com/overview
</a></p>
<p>Seleziona il progetto corretto dall'header in alto, clicca <strong>"Enable"</strong> e attendi qualche minuto prima di riprovare.</p>
</div>
<h5>Importare file</h5>
<p>Stessa procedura di WebDAV: apri <strong>Documenti</strong>, clicca sul repository Drive, sfoglia i file e usa <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span>.</p>
<h5 class="mt-4">⚠️ File Google (Documenti, Fogli, Presentazioni)</h5>
<p>I file creati con Google Workspace (Documenti Google, Fogli Google, Presentazioni Google, Disegni Google) non possono essere scaricati direttamente perché non hanno un formato binario nativo.</p>
<p>Il sistema li converte automaticamente in formato Office Open XML durante l'import:</p>
<table class="table table-sm table-bordered">
<thead><tr><th>File Google</th><th>Formato convertito</th></tr></thead>
<tbody>
<tr><td>Documento Google</td><td><code>.docx</code> (Word)</td></tr>
<tr><td>Foglio Google</td><td><code>.xlsx</code> (Excel)</td></tr>
<tr><td>Presentazione Google</td><td><code>.pptx</code> (PowerPoint)</td></tr>
<tr><td>Disegno Google</td><td><code>.png</code> (Immagine)</td></tr>
<tr><td>Altri (Moduli, Script)</td><td><code>.pdf</code> (PDF)</td></tr>
</tbody>
</table>
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx").
</div>
</div>
</div>
</div>
<div class="tab-pane" id="help-tipologie">
<div class="card card-outline card-success">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-tags mr-2"></i> Tipologie e Gestione Dati</h3>
</div>
<div class="card-body">
<h5>Tipologie Documenti</h5>
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
<p>Si gestiscono in <strong>Impostazioni → Tipologie Documenti</strong>. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).</p>
<h5>Tipologie Individui</h5>
<p>Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in <strong>Impostazioni → Tipologie</strong>.</p>
<h5>Tipologie Eventi</h5>
<p>Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in <strong>Impostazioni → Tipologie Eventi</strong>.</p>
<h5>Ruoli Individui</h5>
<p>Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in <strong>Impostazioni → Ruoli</strong>.</p>
<h5>Visibilità Documenti</h5>
<p>Ogni documento può avere diversi livelli di visibilità:</p>
<ul>
<li><strong>Pubblico</strong> — visibile a tutti gli utenti</li>
<li><strong>Individuo</strong> — visibile solo a uno specifico individuo</li>
<li><strong>Gruppo</strong> — visibile solo ai membri di un gruppo</li>
<li><strong>Evento</strong> — visibile solo ai partecipanti di un evento</li>
<li><strong>Mailing</strong> — visibile solo ai membri di una mailing list</li>
</ul>
</div>
</div>
</div>
<div class="tab-pane" id="help-email">
<div class="card card-outline card-warning">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-envelope mr-2"></i> Email / SMTP</h3>
</div>
<div class="card-body">
<h5>Configurazione Email</h5>
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
<h5>Server IMAP (Ricezione)</h5>
<p>Usato per ricevere e leggere le email direttamente nell'app.</p>
<div class="callout callout-info">
<h6>Campi consigliati per provider comuni</h6>
<table class="table table-sm table-bordered">
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
<tbody>
<tr><td>Gmail</td><td><code>imap.gmail.com</code></td><td><code>993</code></td><td>SSL</td></tr>
<tr><td>Outlook/Office 365</td><td><code>outlook.office365.com</code></td><td><code>993</code></td><td>SSL</td></tr>
<tr><td>Libero</td><td><code>imapmail.libero.it</code></td><td><code>993</code></td><td>SSL</td></tr>
<tr><td>Aruba</td><td><code>imap.aruba.it</code></td><td><code>993</code></td><td>SSL</td></tr>
</tbody>
</table>
</div>
<h5>Server SMTP (Invio)</h5>
<p>Usato per inviare email dall'app. Supporta account multipli (mittenti).</p>
<div class="callout callout-info">
<h6>Campi consigliati per provider comuni</h6>
<table class="table table-sm table-bordered">
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
<tbody>
<tr><td>Gmail</td><td><code>smtp.gmail.com</code></td><td><code>587</code></td><td>TLS</td></tr>
<tr><td>Outlook/Office 365</td><td><code>smtp.office365.com</code></td><td><code>587</code></td><td>TLS</td></tr>
<tr><td>Libero</td><td><code>smtp.libero.it</code></td><td><code>465</code></td><td>SSL</td></tr>
<tr><td>Aruba</td><td><code>smtp.aruba.it</code></td><td><code>465</code></td><td>SSL</td></tr>
</tbody>
</table>
</div>
<h5>Gmail / Google Workspace</h5>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
Gmail richiede una <strong>Password per l'app</strong> (non la password normale) se usi la verifica in due passaggi. Puoi generarla su
<a href="https://myaccount.google.com/apppasswords" target="_blank">https://myaccount.google.com/apppasswords</a>.
</div>
<h5>Mailing</h5>
<p>Le <strong>Mailing List</strong> permettono di inviare email a gruppi predefiniti di destinatari.</p>
<ul>
<li>Crea una mailing list in <strong>Mailing List → Nuova</strong></li>
<li>Puoi popolare la lista da individui, gruppi o manualmente</li>
<li>Invia email alla lista da <strong>Email → Nuova Email</strong>, selezionando la mailing list come destinatario</li>
</ul>
<h5>Test connessione</h5>
<p>Dopo aver configurato i server, usa il pulsante <span class="badge badge-success"><i class="fas fa-vial"></i> Test Connessione</span> per verificare che IMAP e SMTP funzionino correttamente.</p>
</div>
</div>
</div>
<div class="tab-pane" id="help-backup">
<div class="card card-outline card-danger">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i> Backup e Migrazione Server</h3>
</div>
<div class="card-body">
<h5>Cos'è</h5>
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
<h5>Cosa include il backup</h5>
<table class="table table-sm table-bordered">
<thead><tr><th>Componente</th><th>Incluso</th><th>Dettagli</th></tr></thead>
<tbody>
<tr><td>Database (MySQL)</td><td><span class="badge badge-success">Sempre</span></td><td>Struttura, dati, stored procedure, eventi, trigger</td></tr>
<tr><td>File caricati</td><td><span class="badge badge-success">Opzionale</span></td><td>Documenti, avatar, gallerie</td></tr>
<tr><td>Configurazione .env</td><td><span class="badge badge-success">Opzionale</span></td><td>APP_KEY, credenziali DB, SMTP, token OAuth</td></tr>
<tr><td>Metadati</td><td><span class="badge badge-success">Sempre</span></td><td>Versione app, PHP, Laravel</td></tr>
</tbody>
</table>
<h5 class="mt-4">Come eseguire un backup</h5>
<div class="row">
<div class="col-md-6">
<div class="callout callout-info">
<h6>Via Interfaccia Web</h6>
<ol class="mb-0">
<li>Vai su <strong>Admin → Backup</strong></li>
<li>Verifica le impostazioni (includi files/.env)</li>
<li>Clicca <span class="badge badge-primary"><i class="fas fa-play"></i> Esegui Backup Ora</span></li>
<li>Scarica il file ZIP generato</li>
</ol>
</div>
</div>
<div class="col-md-6">
<div class="callout callout-success">
<h6>Via Comando</h6>
<pre class="mb-0"><code>php artisan backup:run</code></pre>
<p class="mb-0 mt-2 small">Il file viene creato in <code>storage/app/backups/</code></p>
</div>
</div>
</div>
<h5 class="mt-4">Backup automatico</h5>
<p>Puoi abilitare il backup automatico dalla pagina <strong>Admin → Backup</strong>:</p>
<ul>
<li><strong>Frequenza:</strong> giornaliero, settimanale o mensile</li>
<li><strong>Ora:</strong> consigliata 03:00 (notte)</li>
<li><strong>Requisito:</strong> il cron di Laravel deve essere configurato sul server</li>
</ul>
<div class="alert alert-warning">
<i class="fas fa-clock mr-2"></i>
Per il backup automatico, aggiungi questo cron:
<pre class="mt-2 mb-0"><code>* * * * * cd <?php echo e(base_path()); ?> && php artisan schedule:run >> /dev/null 2>&1</code></pre>
</div>
<hr class="my-4">
<h4><i class="fas fa-truck mr-2"></i> Procedura di Migrazione Completa</h4>
<p>Segui questa procedura per migrare l'applicazione su un nuovo server LAMP (Linux, Apache, MySQL, PHP).</p>
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>ATTENZIONE: APP_KEY</strong>
<p class="mb-0 mt-2">L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).</p>
<ul class="mt-2">
<li><strong>Se hai il .env originale dal backup:</strong> NON rigenerare APP_KEY — mantieni i dati criptati accessibili.</li>
<li><strong>Se hai perso il .env:</strong> DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.</li>
</ul>
</div>
<h5 class="mt-4">Passo 1: Preparazione del server</h5>
<pre><code>sudo apt update && sudo apt upgrade -y</code></pre>
<h5 class="mt-4">Passo 2: Installa LAMP + PHP 8.4</h5>
<pre><code>sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
composer git unzip
sudo a2enmod rewrite</code></pre>
<h5 class="mt-4">Passo 3: Ottieni l'applicazione</h5>
<div class="callout callout-info py-2">
<i class="fas fa-info-circle"></i>
Puoi clonare il repository con git oppure estrarre il file ZIP ricevuto nella cartella <code>/var/www/glastree</code>.
</div>
<pre><code>cd /var/www
# Opzione A — Git clone
git clone &lt;URL_REPOSITORY&gt; glastree
# Opzione B — ZIP (se non hai git)
# unzip /percorso/del/glastree.zip -d glastree
cd glastree
composer install --no-dev --optimize-autoloader
npm ci && npm run build</code></pre>
<h5 class="mt-4">Passo 4: Carica il backup</h5>
<p>Copia il file <code>backup.zip</code> sul nuovo server e estrailo:</p>
<pre><code>mkdir -p /tmp/restore
unzip backup.zip -d /tmp/restore/
ls -la /tmp/restore/
# Dovresti vedere: database.sql .env files/ manifest.json</code></pre>
<h5 class="mt-4">Passo 5: Crea database e importa</h5>
<pre><code>sudo mysql -u root &lt;&lt;EOF
CREATE DATABASE glastree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'glastree'@'localhost' IDENTIFIED BY 'ScegliPasswordFortep4ss!';
GRANT ALL ON glastree.* TO 'glastree'@'localhost';
FLUSH PRIVILEGES;
EOF
mysql -u glastree -p glastree &lt; /tmp/restore/database.sql</code></pre>
<h5 class="mt-4">Passo 6: Configura .env</h5>
<pre><code>cp /tmp/restore/.env .env
# Modifica se necessario:
# APP_URL=https://nuovo-dominio.it
# DB_HOST=localhost
# DB_DATABASE=glastree
# DB_USERNAME=glastree
# DB_PASSWORD=ScegliPasswordFortep4ss!
# DECIDI SULL'APP_KEY (leggi avviso sopra!)
# php artisan key:generate ← SOLO se non hai il .env originale</code></pre>
<h5 class="mt-4">Passo 7: Ripristina file e permessi</h5>
<pre><code>cp -r /tmp/restore/files/* storage/app/documenti/
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 755 storage bootstrap/cache
php artisan storage:link</code></pre>
<h5 class="mt-4">Passo 8: Cache</h5>
<pre><code>php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache</code></pre>
<h5 class="mt-4">Passo 9: Configura Apache</h5>
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf &lt;&lt;APACHE
&lt;VirtualHost *:80&gt;
ServerName glastree.esempio.it
DocumentRoot /var/www/glastree/public
&lt;Directory /var/www/glastree/public&gt;
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
&lt;/Directory&gt;
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
&lt;/VirtualHost&gt;
APACHE
sudo a2dissite 000-default.conf
sudo a2ensite glastree.conf
sudo systemctl reload apache2</code></pre>
<h5 class="mt-4">Passo 10: SSL Let's Encrypt</h5>
<pre><code>sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d glastree.esempio.it</code></pre>
<h5 class="mt-4">Passo 11: Cron per backup automatico</h5>
<pre><code>sudo crontab -u www-data -e
# Inserisci:
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
<h5 class="mt-4">Passo 12: Verifica finale</h5>
<ol>
<li>Apri https://glastree.esempio.it nel browser</li>
<li>Fai login con le credenziali esistenti</li>
<li>Verifica: Dashboard, Documenti, Impostazioni, Email</li>
<li>Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive</li>
</ol>
<hr class="my-4">
<h5>Comandi Artisan</h5>
<p>Per installazione e ripristino usa i comandi Artisan da terminale. Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
</div>
</div>
</div>
<div class="tab-pane" id="help-docker">
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title"><i class="fab fa-docker mr-2"></i> Docker</h3>
</div>
<div class="card-body">
<h5>Creare un'immagine Docker con dati di base</h5>
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
<div class="callout callout-info">
<i class="fas fa-info-circle"></i>
<strong>Ottenere il codice:</strong> puoi clonare il repository con <code>git clone &lt;URL&gt; glastree</code> oppure, se non hai git, estrarre il file ZIP ricevuto con <code>unzip glastree.zip -d glastree</code> e posizionarti nella cartella <code>glastree</code> prima di procedere.
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="callout callout-info">
<h6><i class="fas fa-database"></i> SQLite (Leggero)</h6>
<p class="mb-0 small">Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.</p>
</div>
</div>
<div class="col-md-6">
<div class="callout callout-success">
<h6><i class="fas fa-server"></i> MySQL (Standard)</h6>
<p class="mb-0 small">Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.</p>
</div>
</div>
</div>
<h5 class="mt-4">1. Dockerfile (comune a entrambe le modalita)</h5>
<p>Crea un file <code>Dockerfile</code> nella root del progetto:</p>
<pre><code>FROM php:8.4-apache
# Abilita mod_rewrite
RUN docker-php-ext-enable opcache \
&& a2enmod rewrite
# Installa estensioni PHP (sia SQLite che MySQL)
RUN docker-php-ext-install pdo_mysql pdo_sqlite bcmath gd zip
# Installa Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Configura DocumentRoot per Laravel
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
# Copia l'applicazione
COPY . /var/www/html/
# Installa dipendenze e compila asset
RUN composer install --no-dev --optimize-autoloader \
&& mkdir -p storage/app/public \
&& mkdir -p storage/framework/cache/data \
&& mkdir -p storage/framework/sessions \
&& mkdir -p storage/framework/views \
&& mkdir -p storage/logs \
&& mkdir -p bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache
# Entrypoint per inizializzazione
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["apache2-foreground"]</code></pre>
<h5 class="mt-4">2. Entrypoint Script (comune)</h5>
<p>Crea <code>docker-entrypoint.sh</code>. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:</p>
<pre><code>#!/bin/bash
set -e
# Se .env non esiste, lo genera dalle variabili d'ambiente
if [ ! -f .env ]; then
cp .env.example .env
if [ "$DB_CONNECTION" = "mysql" ]; then
# Configura MySQL
sed -i "s/DB_CONNECTION=mysql/DB_CONNECTION=mysql/" .env
sed -i "s/DB_HOST=.*/DB_HOST=${DB_HOST:-mysql}/" .env
sed -i "s/DB_PORT=.*/DB_PORT=${DB_PORT:-3306}/" .env
sed -i "s/DB_DATABASE=.*/DB_DATABASE=${DB_DATABASE:-glastree}/" .env
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${DB_USERNAME:-glastree}/" .env
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASSWORD:-secret}/" .env
# Attende che MySQL sia pronto
echo "Attendo MySQL..."
until php -r "new PDO('mysql:host=${DB_HOST:-mysql};dbname=${DB_DATABASE:-glastree}', '${DB_USERNAME:-glastree}', '${DB_PASSWORD:-secret}');" 2>/dev/null; do
sleep 2
done
echo "MySQL pronto."
else
# Configura SQLite
sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env
sed -i 's/DB_HOST=.*//' .env
sed -i 's/DB_PORT=.*//' .env
sed -i 's/DB_DATABASE=.*/DB_DATABASE=\/var\/www\/html\/database\/database.sqlite/' .env
sed -i 's/DB_USERNAME=.*//' .env
sed -i 's/DB_PASSWORD=.*//' .env
touch database/database.sqlite
chmod 664 database/database.sqlite
fi
# Genera APP_KEY
php artisan key:generate --force
fi
# Esegui migration e seed se il database e vuoto
if [ ! -f .db_initialized ]; then
php artisan migrate --force
php artisan db:seed --force
touch .db_initialized
fi
# Configura storage link
php artisan storage:link --force 2>/dev/null || true
exec "$@"</code></pre>
<pre><code>chmod +x docker-entrypoint.sh</code></pre>
<h5 class="mt-4">3. Seed Base Dati (comune)</h5>
<p>Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:</p>
<pre><code>php artisan make:seeder DockerBaseDataSeeder</code></pre>
<p>Contenuto di <code>database/seeders/DockerBaseDataSeeder.php</code>:</p>
<pre><code>&lt;?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class DockerBaseDataSeeder extends Seeder
{
public function run(): void
{
// Tipologie Documenti
$tipologieDoc = ['documento', 'avatar', 'galleria', 'statuto', 'programma', 'locandina', 'altro'];
foreach ($tipologieDoc as $ord => $nome) {
DB::table('tipologie_documenti')->updateOrInsert(
['nome' => $nome],
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
);
}
// Tipologie Eventi
$tipologieEventi = [
['nome' => 'catechesi', 'descrizione' => 'Catechesi'],
['nome' => 'liturgia', 'descrizione' => 'Liturgia'],
['nome' => 'animazione', 'descrizione' => 'Animazione'],
['nome' => 'formazione', 'descrizione' => 'Formazione'],
['nome' => 'incontro', 'descrizione' => 'Incontro'],
['nome' => 'riunione', 'descrizione' => 'Riunione'],
['nome' => 'festa', 'descrizione' => 'Festa'],
['nome' => 'altro', 'descrizione' => 'Altro'],
];
foreach ($tipologieEventi as $ord => $t) {
DB::table('tipologie_eventi')->updateOrInsert(
['nome' => $t['nome']],
['descrizione' => $t['descrizione'], 'ordine' => $ord + 1, 'attiva' => true]
);
}
// Ruoli
$ruoli = ['presidente', 'vicepresidente', 'segretario', 'tesoriere', 'consigliere', 'volontario', 'socio', 'altro'];
foreach ($ruoli as $ord => $nome) {
DB::table('ruoli')->updateOrInsert(
['nome' => $nome],
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
);
}
}
}</code></pre>
<p>Registralo in <code>database/seeders/DatabaseSeeder.php</code>:</p>
<pre><code>$this->call([
// ... altri seeder ...
DockerBaseDataSeeder::class,
]);</code></pre>
<h5 class="mt-4">4. Docker Compose</h5>
<h6 class="text-info"><i class="fas fa-database mr-1"></i> Opzione A — SQLite (leggero)</h6>
<pre><code>services:
glastree:
build: .
ports:
- "8080:80"
volumes:
- glastree_storage:/var/www/html/storage
- glastree_db:/var/www/html/database
environment:
- APP_ENV=production
- APP_DEBUG=false
- APP_URL=http://localhost:8080
- DB_CONNECTION=sqlite
volumes:
glastree_storage:
glastree_db:</code></pre>
<h6 class="mt-3 text-success"><i class="fas fa-server mr-1"></i> Opzione B — MySQL (standard)</h6>
<pre><code>services:
glastree:
build: .
ports:
- "8080:80"
volumes:
- glastree_storage:/var/www/html/storage
depends_on:
mysql:
condition: service_healthy
environment:
- APP_ENV=production
- APP_DEBUG=false
- APP_URL=http://localhost:8080
- DB_CONNECTION=mysql
- DB_HOST=mysql
- DB_PORT=3306
- DB_DATABASE=glastree
- DB_USERNAME=glastree
- DB_PASSWORD=secret
mysql:
image: mysql:8.0
volumes:
- mysql_data:/var/lib/mysql
environment:
- MYSQL_DATABASE=glastree
- MYSQL_USER=glastree
- MYSQL_PASSWORD=secret
- MYSQL_ROOT_PASSWORD=root_secret
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
volumes:
glastree_storage:
mysql_data:</code></pre>
<h5 class="mt-4">5. Build e avvio</h5>
<p>Assicurati di essere nella cartella del progetto (ottenuta via <code>git clone</code> o estraendo il ZIP), poi:</p>
<pre><code># Costruisci l'immagine (comune)
docker build -t glastree:latest .
# Avvia con SQLite
docker compose -f docker-compose.yml up -d
# Oppure avvia con MySQL
# docker compose -f docker-compose.mysql.yml up -d
# Apri nel browser
open http://localhost:8080</code></pre>
<h5 class="mt-4">6. Primo accesso</h5>
<p>Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:</p>
<ul>
<li><strong>Tipologie Documenti:</strong> documento, avatar, galleria, statuto, programma, locandina, altro</li>
<li><strong>Tipologie Eventi:</strong> catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro</li>
<li><strong>Ruoli:</strong> presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro</li>
</ul>
<p>Dovrai creare il primo utente amministratore via CLI:</p>
<pre><code>docker exec -it glastree php artisan tinker --execute="
\$u = new \App\Models\User;
\$u->name = 'Amministratore';
\$u->email = 'admin@esempio.it';
\$u->password = bcrypt('password-forte');
\$u->is_admin = true;
\$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2];
\$u->save();
echo 'Utente creato con successo!';
"</code></pre>
<div class="callout callout-warning mt-4">
<h6><i class="fas fa-exclamation-triangle"></i> Note importanti</h6>
<ul class="mb-0">
<li><strong>Persistenza:</strong> i volumi Docker preservano storage e database tra i riavvii</li>
<li><strong>APP_KEY:</strong> viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili</li>
<li><strong>MySQL in produzione:</strong> imposta password sicure per <code>MYSQL_PASSWORD</code> e <code>MYSQL_ROOT_PASSWORD</code></li>
<li><strong>File compose separati:</strong> puoi mantenere entrambi i file (<code>docker-compose.yml</code> per SQLite, <code>docker-compose.mysql.yml</code> per MySQL) e scegliere con <code>-f</code></li>
</ul>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="help-installazione">
<div class="card card-outline card-dark">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-terminal mr-2"></i> Guida all'Installazione</h3>
</div>
<div class="card-body">
<h5>Panoramica</h5>
<p>L'installazione avviene tramite l'interattivo <code>install.php</code> che guida passo-passo. Supporta due modalita:</p>
<div class="row">
<div class="col-md-6">
<div class="callout callout-info">
<h6><i class="fas fa-rocket"></i> Fresh Install</h6>
<p class="mb-0">Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.</p>
</div>
</div>
<div class="col-md-6">
<div class="callout callout-success">
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
<p class="mb-0">Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.</p>
</div>
</div>
</div>
<h5 class="mt-4">Requisiti del Server</h5>
<table class="table table-sm table-bordered">
<thead><tr><th>Componente</th><th>Versione Minima</th><th>Note</th></tr></thead>
<tbody>
<tr><td>Web Server</td><td>Apache 2.4+</td><td>Con mod_rewrite abilitato</td></tr>
<tr><td>PHP</td><td>8.2+ (consigliato 8.4)</td><td>Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo</td></tr>
<tr><td>MySQL / MariaDB</td><td>MySQL 8.0+ / MariaDB 10.5+</td><td>Supporto utf8mb4</td></tr>
<tr><td>Composer</td><td>2.x</td><td>Per gestione dipendenze PHP</td></tr>
<tr><td>Node.js / npm</td><td>Node 20+ / npm 10+</td><td>Per compilazione asset frontend (opzionale se precompilati)</td></tr>
</tbody>
</table>
<h5 class="mt-4">Installazione Passo-Passo</h5>
<h6>Passo 1: Preparare il server LAMP</h6>
<pre><code>sudo apt update && sudo apt upgrade -y
sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
composer git unzip
sudo a2enmod rewrite
sudo systemctl restart apache2</code></pre>
<h6>Passo 2: Ottenere l'applicazione</h6>
<div class="callout callout-info py-2">
<i class="fas fa-info-circle"></i>
Puoi clonare il repository con git oppure estrarre il file ZIP ricevuto nella cartella <code>/var/www/glastree</code>.
</div>
<pre><code>cd /var/www
# Opzione A — Git clone
git clone &lt;URL_REPOSITORY&gt; glastree
# Opzione B — ZIP (se non hai git)
# unzip /percorso/del/glastree.zip -d glastree
cd glastree</code></pre>
<h6>Passo 3: Eseguire l'installer</h6>
<p>Lancerai l'installer interattivo che ti guidera attraverso tutte le fasi:</p>
<pre><code>php install.php</code></pre>
<p>L'installer ti chiedera:</p>
<ol>
<li><strong>Modalità</strong>: Fresh Install (Apache/Docker) o Restore da Backup</li>
<li><strong>Parametri generali</strong>: nome sito, amministratore (nome, email, password), URL pubblico</li>
<li><strong>Database</strong>: MySQL (host, porta, nome, utente, password) o SQLite</li>
</ol>
<p>L'installer esegue automaticamente:</p>
<ul>
<li>Generazione <code>.env</code> e <code>APP_KEY</code></li>
<li>Creazione del database</li>
<li><code>composer install</code> e registrazione pacchetti</li>
<li>Migration e seed dei dati di base</li>
<li>Creazione dell'utente amministratore</li>
<li>Compilazione asset frontend (se npm disponibile)</li>
<li>Impostazione permessi cartelle</li>
</ul>
<div class="callout callout-info">
<h6><i class="fas fa-info-circle"></i> Docker</h6>
<p>Se scegli la modalita Docker, l'installer builda l'immagine, avvia i container e crea l'admin all'interno del container.</p>
</div>
<div class="callout callout-success">
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
<p>Se scegli Restore, l'installer ti chiedera il percorso del file ZIP di backup, ripristinera il database, i file e la configurazione.</p>
</div>
<h6 class="mt-4">Passo 4: Configurare Apache (solo modalita Fresh Install)</h6>
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf &lt;&lt;APACHE
&lt;VirtualHost *:80&gt;
ServerName glastree.esempio.it
DocumentRoot /var/www/glastree/public
&lt;Directory /var/www/glastree/public&gt;
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
&lt;/Directory&gt;
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
&lt;/VirtualHost&gt;
APACHE
sudo a2dissite 000-default.conf
sudo a2ensite glastree.conf
sudo systemctl reload apache2</code></pre>
<i class="fas fa-exclamation-triangle"></i>
<strong>Importante:</strong> Il restore da backup richiede che tu abbia un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale. Estrai manualmente il contenuto seguendo la struttura del progetto.
</div>
<h5 class="mt-4">Cosa succede durante l'installazione</h5>
<p>I comandi <code>php artisan migrate --seed</code> eseguono:</p>
<ol>
<li>Genera <code>APP_KEY</code> per la crittografia (gia eseguito al passo 5)</li>
<li>Esegue tutte le migration per creare le tabelle del database</li>
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
<li>Crea il collegamento <code>storage &rarr; public/storage</code></li>
<li>Crea l'utente amministratore con i permessi completi</li>
</ol>
<h5 class="mt-4">Restore da Backup</h5>
<p>Il backup ZIP generato dalla pagina <strong>Admin &rarr; Backup</strong> contiene un dump SQL e i file dei documenti. Per ripristinare:</p>
<ul>
<li>Estrai il file ZIP in una cartella temporanea</li>
<li>Importa il database dal file SQL (<code>mysql -u root -p glastree &lt; backup/database.sql</code>)</li>
<li>Copia i file in <code>storage/app/</code> per ripristinare i documenti</li>
<li>Copia il file <code>.env</code> dal backup (modifica le credenziali DB per il nuovo server)</li>
<li>Esegui <code>php artisan key:generate</code> se la <code>APP_KEY</code> del backup non è utilizzabile</li>
</ul>
<h5 class="mt-4">Dopo l'Installazione</h5>
<ol>
<li><strong>Configura SSL</strong> con Let's Encrypt:
<pre><code>sudo apt install -y certbot python3-certbot-apache
sudo certbot --apache -d glastree.esempio.it</code></pre>
</li>
<li><strong>Accedi</strong> con le credenziali amministratore create durante l'installazione</li>
<li><strong>Configura l'applicazione</strong> in Impostazioni:
<ul>
<li>Nome applicazione e logo</li>
<li>Email (SMTP per invio, IMAP per ricezione)</li>
<li>Repository remoti (Google Drive, Nextcloud)</li>
</ul>
</li>
<li><strong>Crea utenti</strong> dal pannello Admin &rarr; Utenti</li>
<li><strong>Configura il cron</strong> per backup automatico e sync email:
<pre><code>sudo crontab -u www-data -e
# Inserisci:
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
</li>
</ol>
<h5 class="mt-4">Risoluzione Problemi</h5>
<div class="callout callout-danger">
<h6>Errore "500 | Server Error" dopo l'installazione</h6>
<p>Probabilmente mancano permessi sulle cartelle <code>storage</code> o <code>bootstrap/cache</code>:</p>
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache</code></pre>
</div>
<div class="callout callout-danger">
<h6>Errore "No application encryption key"</h6>
<p>APP_KEY non generata. Nella cartella dell'app:</p>
<pre><code>php artisan key:generate</code></pre>
</div>
<div class="callout callout-warning">
<h6>mod_rewrite non funziona</h6>
<p>Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:</p>
<pre><code>sudo a2enmod rewrite
# Nel VirtualHost: AllowOverride All
sudo systemctl reload apache2</code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.nav-pills .nav-link { border-radius: 0; }
.nav-pills .nav-link.active { background-color: #007bff; }
.callout { border-left: 3px solid; padding: 12px 16px; margin: 16px 0; background: #f8f9fa; }
.callout-info { border-color: #17a2b8; }
.callout-danger { border-color: #dc3545; }
.callout-warning { border-color: #ffc107; }
</style>
<?php $__env->stopSection(); ?>
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/help/index.blade.php ENDPATH**/ ?>
+28
View File
@@ -0,0 +1,28 @@
<?php
use App\Models\EmailSetting;
use App\Models\User;
beforeEach(function () {
EmailSetting::where('imap_host', 'test.feature')->delete();
});
it('saves email settings via POST form', function () {
$user = User::first();
$response = $this->actingAs($user)->post(route('impostazioni.email.save'), [
'imap_host' => 'test.feature',
'imap_port' => '993',
'imap_encryption' => 'ssl',
'imap_username' => 'featureuser',
'imap_password' => 'featurepass',
'email_address' => 'feature@test.com',
'email_name' => 'Feature Test',
'is_active' => '1',
]);
$response->assertSessionHas('success');
$record = EmailSetting::where('imap_host', 'test.feature')->first();
expect($record)->not->toBeNull();
expect($record->email_address)->toBe('feature@test.com');
});
+1
View File
@@ -7,6 +7,7 @@ $baseDir = dirname($vendorDir);
return array( return array(
'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php', 'App\\Console\\Commands\\BackupRunCommand' => $baseDir . '/app/Console/Commands/BackupRunCommand.php',
'App\\Console\\Commands\\DiagnoseEmail' => $baseDir . '/app/Console/Commands/DiagnoseEmail.php',
'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php', 'App\\Console\\Commands\\ExportHelpPdf' => $baseDir . '/app/Console/Commands/ExportHelpPdf.php',
'App\\Console\\Commands\\ResetUserPassword' => $baseDir . '/app/Console/Commands/ResetUserPassword.php', 'App\\Console\\Commands\\ResetUserPassword' => $baseDir . '/app/Console/Commands/ResetUserPassword.php',
'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php', 'App\\Console\\Commands\\SyncUserPermissions' => $baseDir . '/app/Console/Commands/SyncUserPermissions.php',
+1
View File
@@ -619,6 +619,7 @@ class ComposerStaticInit74199c0a3889e8d1256a25f93805ef56
public static $classMap = array ( public static $classMap = array (
'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php', 'App\\Console\\Commands\\BackupRunCommand' => __DIR__ . '/../..' . '/app/Console/Commands/BackupRunCommand.php',
'App\\Console\\Commands\\DiagnoseEmail' => __DIR__ . '/../..' . '/app/Console/Commands/DiagnoseEmail.php',
'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php', 'App\\Console\\Commands\\ExportHelpPdf' => __DIR__ . '/../..' . '/app/Console/Commands/ExportHelpPdf.php',
'App\\Console\\Commands\\ResetUserPassword' => __DIR__ . '/../..' . '/app/Console/Commands/ResetUserPassword.php', 'App\\Console\\Commands\\ResetUserPassword' => __DIR__ . '/../..' . '/app/Console/Commands/ResetUserPassword.php',
'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php', 'App\\Console\\Commands\\SyncUserPermissions' => __DIR__ . '/../..' . '/app/Console/Commands/SyncUserPermissions.php',
+2 -2
View File
@@ -3,7 +3,7 @@
'name' => 'laravel/laravel', 'name' => 'laravel/laravel',
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'f9c1268466e9f534c2f1ade6e5bb4bddfbaa56e5', 'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -427,7 +427,7 @@
'laravel/laravel' => array( 'laravel/laravel' => array(
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'f9c1268466e9f534c2f1ade6e5bb4bddfbaa56e5', 'reference' => '90093a086b35dce8edd97fce09d32d2be42853e8',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),