# MOSAIC KI-Modulvorlagen

## Zweck

Diese Datei ist fuer KI-Systeme gedacht.

Sie enthaelt direkt nutzbare Vorlagen fuer:

- `grid`
- `form`
- `menu`
- `report`
- Untertabellen
- Hook-Dateien

Die Vorlagen folgen dem Kernstand April 2026.

Die formale und oeffentliche Konfigurations-Spezifikation steht in:

- `MOSAIC_Config_Spezifikation.md`

Diese Datei liefert Vorlagen und Muster, ersetzt aber nicht die verbindliche Schluessel- und Strukturdefinition aus der Spezifikation.

## 1. Runner-Mapping

- `*_grid.cfg` -> `grid.php`
- `*_form.cfg` -> `form.php`
- `*_egrid.cfg` -> `show_egrid.php`
- `*_report.cfg` -> `show_report.php`
- `*_forum.cfg` -> `show_forum.php`
- `*_menu.cfg` -> `show_menu.php`
- `*_chart.cfg` -> `show_chart.php`
- `*_connector.cfg` -> `show_connector.php`
- `*_paypal.cfg` -> `show_paypal.php`
- `*_v_menu.cfg` -> `show_v_menu.php`

Nicht mehr verwenden:

- `menu_show.php`
- `dashboard_show.php`
- `chart.php`
- `report.php`
- `login_show.php`
- `v_menu.php`

## 2. Grundregeln

- `primary_key` immer als Array
- Auditfelder in Form unter `System`
- Untertabellen immer auf Elternsatz filtern
- Eltern-`_ref` im Insert vorbelegen
- Delete-Schutz serverseitig in `onBeforeDelete`
- `transaction => true` nur verwenden, wenn echte DB-Transaktionen gewuenscht sind; aktiv unter `mysql + InnoDB`, `pgsql` und `oracle`

## 3. Minimalvorlage `grid.cfg`

```php
<?php

return [
    'limit' => '10,20,50,100,500',
    'width' => '90%',
    'db_typ' => 'mysql',
    'title' => 'Beispiel',
    'sql' => 'SELECT
        `beispiel_id`,
        `bezeichnung`,
        `active`
        FROM `beispiel`
        ORDER BY `bezeichnung` ASC, `beispiel_id` DESC',
    'marker' => true,
    'primary_key' => ['beispiel_id'],
    'autoincrement' => true,
    'edit_link' => 'beispiel',
    'columns' => [
        'beispiel_id' => [
            'label' => 'ID',
            'type' => 'number',
            'sort' => true,
        ],
        'bezeichnung' => [
            'label' => 'Bezeichnung',
            'type' => 'text',
            'quicksearch' => true,
            'sort' => true,
        ],
        'active' => [
            'label' => 'Aktiv',
            'type' => 'checkbox',
            'sort' => true,
        ],
    ],
    'permissions' => [
        'xls_export' => true,
        'pdf_export' => true,
        'edit' => true,
        'add' => true,
        'delete' => false,
        'copy' => false,
        'view' => true,
    ],
];
```

## 4. Grid mit Advanced Search

```php
'columns' => [
    'bezeichnung' => [
        'label' => 'Bezeichnung',
        'type' => 'text',
        'quicksearch' => true,
        'sort' => true,
    ],
    'gruppe_suche' => [
        'label' => 'Gruppe Suche',
        'type' => 'text',
        'row_display' => false,
        'sort' => false,
    ],
],
'filters' => [
    ['header' => 'Allgemein'],
    [
        'field' => 'bezeichnung',
        'label' => 'Bezeichnung',
        'type' => 'text',
        'operators' => ['contains', '=', '<>', 'empty'],
    ],
    [
        'field' => 'gruppe_suche',
        'label' => 'Gruppe',
        'type' => 'text',
        'operators' => ['contains', '=', '<>', 'empty'],
    ],
],
```

## 5. Grid mit Session-Bindung

```php
<?php

$sessionFirmaId = (int)($_SESSION['firma_id'] ?? ($_SESSION['firma_ref'] ?? 0));
$where = $sessionFirmaId > 0
    ? 'WHERE `firma_ref` = ' . $sessionFirmaId
    : 'WHERE 1 = 0';

return [
    'title' => 'Firmengebundene Daten',
    'db_typ' => 'mysql',
    'sql' => 'SELECT `beispiel_id`, `firma_ref`, `bezeichnung`, `active`
        FROM `beispiel`
        ' . $where . '
        ORDER BY `bezeichnung`',
    'primary_key' => ['beispiel_id'],
    'edit_link' => 'beispiel',
    'columns' => [
        'beispiel_id' => [
            'label' => 'ID',
            'type' => 'number',
        ],
        'bezeichnung' => [
            'label' => 'Bezeichnung',
            'type' => 'text',
        ],
        'active' => [
            'label' => 'Aktiv',
            'type' => 'checkbox',
        ],
    ],
];
```

## 6. Minimalvorlage `form.cfg`

```php
<?php

return [
    'limit' => 100,
    'width' => '85%',
    'title' => 'Beispiel',
    'table' => 'beispiel',
    'query' => 'SELECT
        `beispiel_id`,
        `bezeichnung`,
        `bemerkung`,
        `created`,
        `updated`,
        `created_by`,
        `updated_by`,
        `active`
        FROM `beispiel`',
    'marker' => true,
    'primary_key' => ['beispiel_id'],
    'autoincrement' => true,
    'transaction' => false,
    'tabs' => [
        'TAB_1' => [
            'label' => 'Stammdaten',
            'title_display' => true,
            'label_display' => true,
            'position' => 'beside',
            'columns' => 2,
            'width' => '100%',
            'collapse' => 'expanded',
            'fields' => [
                'beispiel_id' => [
                    'label' => 'ID',
                    'type' => 'integer',
                    'hidden_on_insert' => true,
                ],
                'bezeichnung' => [
                    'label' => 'Bezeichnung',
                    'type' => 'text',
                    'required' => true,
                    'input_length' => 100,
                ],
                'bemerkung' => [
                    'label' => 'Bemerkung',
                    'type' => 'text_multiline',
                    'input_length' => 65535,
                ],
                'active' => [
                    'label' => 'Aktiv',
                    'type' => 'checkbox',
                    'default_on_insert' => 1,
                ],
            ],
        ],
        'TAB_SYSTEM' => [
            'label' => 'System',
            'title_display' => true,
            'label_display' => true,
            'position' => 'beside',
            'columns' => 2,
            'width' => '100%',
            'collapse' => 'expanded',
            'fields' => [
                'created' => [
                    'label' => 'Erstellt am',
                    'type' => 'datetime',
                    'hidden_on_insert' => true,
                    'readonly_on_insert' => true,
                    'readonly_on_update' => true,
                ],
                'updated' => [
                    'label' => 'Geaendert am',
                    'type' => 'datetime',
                    'hidden_on_insert' => true,
                    'readonly_on_insert' => true,
                    'readonly_on_update' => true,
                ],
                'created_by' => [
                    'label' => 'Erstellt von',
                    'type' => 'select',
                    'hidden_on_insert' => true,
                    'readonly_on_insert' => true,
                    'readonly_on_update' => true,
                    'lookup_method' => 'automatic',
                    'lookup_sql' => 'SELECT `mitarbeiter_id`, CONCAT(`vorname`, \' \', `nachname`) FROM `cl_mitarbeiter` ORDER BY `nachname`, `vorname`',
                ],
                'updated_by' => [
                    'label' => 'Geaendert von',
                    'type' => 'select',
                    'hidden_on_insert' => true,
                    'readonly_on_insert' => true,
                    'readonly_on_update' => true,
                    'lookup_method' => 'automatic',
                    'lookup_sql' => 'SELECT `mitarbeiter_id`, CONCAT(`vorname`, \' \', `nachname`) FROM `cl_mitarbeiter` ORDER BY `nachname`, `vorname`',
                ],
            ],
        ],
    ],
];
```

## 7. Formular mit Session-Bindung

```php
<?php

$sessionFirmaId = (int)($_SESSION['firma_id'] ?? ($_SESSION['firma_ref'] ?? 0));

return [
    'title' => 'Firmengebundener Datensatz',
    'table' => 'beispiel',
    'query' => 'SELECT `beispiel_id`, `firma_ref`, `bezeichnung`, `active`
        FROM `beispiel`
        WHERE `firma_ref` = ' . $sessionFirmaId,
    'primary_key' => ['beispiel_id'],
    'autoincrement' => true,
    'tabs' => [
        'TAB_1' => [
            'label' => 'Stammdaten',
            'fields' => [
                'firma_ref' => [
                    'label' => 'Firma',
                    'type' => 'select',
                    'hidden_on_insert' => true,
                    'hidden_on_update' => true,
                    'default_on_insert' => $sessionFirmaId,
                    'lookup_method' => 'automatic',
                    'lookup_sql' => 'SELECT `firma_id`, `firmenname_1` FROM `cl_firma` ORDER BY `firmenname_1`',
                ],
                'bezeichnung' => [
                    'label' => 'Bezeichnung',
                    'type' => 'text',
                    'required' => true,
                    'input_length' => 100,
                ],
            ],
        ],
    ],
];
```

## 8. Formular mit `transaction`

```php
return [
    'table' => 'auftrag',
    'query' => 'SELECT `auftrag_id`, `bezeichnung`, `active` FROM `auftrag`',
    'primary_key' => ['auftrag_id'],
    'transaction' => true,
    'tabs' => [
        // ...
    ],
];
```

Regel:

- unter `mysql` nur sinnvoll mit Tabellen-Engine `InnoDB`
- unter `pgsql` und `oracle` direkt zulaessig
- bei `MyISAM` keine Rollback-Garantie erwarten

## 9. Select mit Lookup

```php
'hauptkategorie_ref' => [
    'label' => 'Hauptkategorie',
    'type' => 'select',
    'required' => true,
    'lookup_method' => 'automatic',
    'lookup_sql' => 'SELECT `hauptkategorie_id`, `bezeichnung` FROM `hauptkategorie` ORDER BY `bezeichnung`',
],
```

## 10. Abhaengiger Select

```php
'hauptkategorie_ref' => [
    'label' => 'Hauptkategorie',
    'type' => 'select',
    'required' => true,
    'lookup_method' => 'automatic',
    'lookup_sql' => 'SELECT `hauptkategorie_id`, `bezeichnung` FROM `hauptkategorie` ORDER BY `bezeichnung`',
],
'unterkategorie_ref' => [
    'label' => 'Unterkategorie',
    'type' => 'select',
    'lookup_method' => 'automatic',
    'lookup_sql' => 'SELECT `unterkategorie_id`, `bezeichnung` FROM `unterkategorie` WHERE 1 = 0 ORDER BY `bezeichnung`',
    'tooltip' => 'Optional. Wird per AJAX passend zur Hauptkategorie nachgeladen.',
    'ajax' => [
        'handler' => 'ajax_unterkategorie_lookup',
        'trigger' => 'change',
        'ajax_message' => 'Auswahl wird geladen',
    ],
],
```

In der Hook-Datei:

```php
function apply_unterkategorie_lookup(array &$formConfig, int $hauptkategorieId): void
{
    $lookupSql = $hauptkategorieId > 0
        ? 'SELECT `unterkategorie_id`, `bezeichnung` FROM `unterkategorie` WHERE `hauptkategorie_ref` = ' . $hauptkategorieId . ' ORDER BY `bezeichnung`'
        : 'SELECT `unterkategorie_id`, `bezeichnung` FROM `unterkategorie` WHERE 1 = 0 ORDER BY `bezeichnung`';

    foreach (($formConfig['tabs'] ?? []) as $tabKey => $tabConfig) {
        if (!isset($tabConfig['fields']['unterkategorie_ref'])) {
            continue;
        }

        $formConfig['tabs'][$tabKey]['fields']['unterkategorie_ref']['lookup_sql'] = $lookupSql;
        break;
    }
}
```

## 11. Minimalvorlage `grid_function.php`

```php
<?php

function onRow(array &$context, array &$row, array &$gridConfig): void
{
    $row['active'] = (int)($row['active'] ?? 0);
}
```

## 12. Grid-Linkspalte mit HTML

```php
'details_link' => [
    'label' => 'DT',
    'type' => 'html',
    'orientation' => 'center',
    'sort' => false,
],
```

In der Hook-Datei:

```php
function onRow(array &$context, array &$row, array &$gridConfig): void
{
    $id = (int)($row['beispiel_id'] ?? 0);
    $count = (int)($row['detail_anzahl'] ?? 0);

    $row['details_link'] = '<a href="grid.php?modul=detail&modul_path=01_detail&parent_ref=' . $id . '">DT (' . $count . ')</a>';
}
```

## 13. Minimalvorlage `form_function.php`

```php
<?php

function onLoad(array &$context, array &$formConfig): void
{
    $formData = &$context['form_data'];
    $formData['active'] = (int)($formData['active'] ?? 1);
}

function onValidate(array &$context, array &$postData, $currentId): ?string
{
    $postData['bezeichnung'] = trim((string)($postData['bezeichnung'] ?? ''));

    if ($postData['bezeichnung'] === '') {
        return 'Bezeichnung ist ein Pflichtfeld.';
    }

    return null;
}

function onValidateSuccess(array &$context, array &$postData, $currentId): ?string
{
    return null;
}

function onValidateFailure(array &$context, array &$postData, $currentId, array &$invalid, string &$message): void
{
}

function onBeforeInsert(array &$context, array &$postData, $currentId): ?string
{
    $postData['created'] = date('Y-m-d H:i:s');
    return null;
}

function onBeforeUpdate(array &$context, array &$postData, $currentId): ?string
{
    $postData['updated'] = date('Y-m-d H:i:s');
    return null;
}
```

## 14. Delete-Schutz in `onBeforeDelete`

```php
function count_dependent_children(PDO $db, int $parentId): int
{
    $stmt = $db->prepare(
        'SELECT COUNT(*)
         FROM `child_table`
         WHERE `parent_ref` = :parent_ref'
    );
    $stmt->execute([':parent_ref' => $parentId]);

    return (int)$stmt->fetchColumn();
}

function onBeforeDelete(array &$context, array &$formConfig): void
{
    $db = $context['db'] ?? null;
    $id = (int)($context['id'] ?? 0);

    if (!($db instanceof PDO) || $id <= 0) {
        return;
    }

    $count = count_dependent_children($db, $id);
    if ($count > 0) {
        throw new RuntimeException(deepl_translate('Loeschen nicht erlaubt, weil noch abhaengige Daten vorhanden sind.'));
    }
}
```

## 15. Untertabelle: Grid nur im Elternkontext

```php
<?php

$parentRef = (int)($_GET['parent_ref'] ?? 0);
$where = $parentRef > 0
    ? 'WHERE `parent_ref` = ' . $parentRef
    : 'WHERE 1 = 0';

return [
    'title' => 'Unterdaten',
    'db_typ' => 'mysql',
    'sql' => 'SELECT `child_id`, `parent_ref`, `bezeichnung`, `active`
        FROM `child_table`
        ' . $where . '
        ORDER BY `child_id` DESC',
    'primary_key' => ['child_id'],
    'edit_link' => 'child',
    'columns' => [
        'child_id' => [
            'label' => 'ID',
            'type' => 'number',
        ],
        'bezeichnung' => [
            'label' => 'Bezeichnung',
            'type' => 'text',
        ],
        'active' => [
            'label' => 'Aktiv',
            'type' => 'checkbox',
        ],
    ],
];
```

## 16. Untertabelle: Formular mit vorbelegtem Eltern-Ref

```php
<?php

$parentRef = (int)($_GET['parent_ref'] ?? 0);

return [
    'title' => 'Unterdatensatz',
    'table' => 'child_table',
    'query' => 'SELECT `child_id`, `parent_ref`, `bezeichnung`, `active`
        FROM `child_table`
        WHERE `parent_ref` = ' . $parentRef,
    'primary_key' => ['child_id'],
    'autoincrement' => true,
    'tabs' => [
        'TAB_1' => [
            'label' => 'Stammdaten',
            'fields' => [
                'parent_ref' => [
                    'label' => 'Parent',
                    'type' => 'integer',
                    'hidden_on_insert' => true,
                    'hidden_on_update' => true,
                    'default_on_insert' => $parentRef,
                ],
                'bezeichnung' => [
                    'label' => 'Bezeichnung',
                    'type' => 'text',
                    'required' => true,
                    'input_length' => 100,
                ],
                'active' => [
                    'label' => 'Aktiv',
                    'type' => 'checkbox',
                    'default_on_insert' => 1,
                ],
            ],
        ],
    ],
];
```

## 17. Untertabelle: Hook fuer Elternbindung

```php
<?php

function onLoad(array &$context, array &$formConfig): void
{
    $parentRef = (int)($_GET['parent_ref'] ?? 0);
    $currentId = (int)($context['id'] ?? 0);
    $formData = &$context['form_data'];

    if ($currentId <= 0 && $parentRef > 0) {
        $formData['parent_ref'] = $parentRef;
    }
}

function onBeforeInsert(array &$context, array &$postData, array &$formConfig): void
{
    $parentRef = (int)($_GET['parent_ref'] ?? 0);
    if ($parentRef > 0) {
        $postData['parent_ref'] = $parentRef;
    }
}

function onBeforeUpdate(array &$context, array &$postData, array &$formConfig): void
{
    $db = $context['db'] ?? null;
    $id = (int)($context['id'] ?? 0);

    if (!($db instanceof PDO) || $id <= 0) {
        return;
    }

    $stmt = $db->prepare('SELECT `parent_ref` FROM `child_table` WHERE `child_id` = :id');
    $stmt->execute([':id' => $id]);
    $postData['parent_ref'] = (int)$stmt->fetchColumn();
}
```

## 18. Minimalvorlage `menu.cfg`

```php
<?php

$rootUrl = rtrim((string)($_SESSION['APP_ROOT_URL'] ?? '/'), '/') . '/';
$project = trim((string)($_SESSION['projekt'] ?? 'demo'), '/');
$projectBase = $rootUrl . ($project !== '' ? $project . '/' : '');
$menuBase = $projectBase . 'menu/';

return [
    'title' => 'Demo',
    'orientation' => 'horizontal',
    'breadcrumb' => true,
    'breadcrumb_clickable' => true,
    'items' => [
        [
            'type' => 'item',
            'id' => 'start',
            'label' => 'Start',
            'icon' => 'fas fa-home',
            'hint' => 'Startseite',
            'target' => '_self',
            'runner' => $menuBase . 'start.php',
            'modul' => '',
            'modul_path' => '',
            'params' => '',
            'children' => [],
        ],
        [
            'type' => 'group',
            'id' => 'stamm',
            'label' => '01. Stamm',
            'icon' => 'fas fa-folder-open',
            'hint' => 'Stammdaten',
            'children' => [
                [
                    'type' => 'item',
                    'id' => 'beispiel_liste',
                    'label' => '01. Beispiele',
                    'icon' => 'fas fa-list',
                    'hint' => 'Beispieltabelle',
                    'target' => '_self',
                    'runner' => 'grid.php',
                    'modul' => 'beispiel',
                    'modul_path' => '01_stamm/01_beispiel',
                    'params' => '',
                    'children' => [],
                ],
            ],
        ],
    ],
];
```

## 19. Minimalvorlage `report.cfg`

```php
<?php

return [
    'app' => [
        'type' => 'report',
        'version' => 1,
        'title' => 'Beispielreport',
    ],
    'description' => 'Einfacher filterbarer Bericht',
    'hooks_file' => 'beispiel_report_function.php',
    'data' => [
        'database' => 'default',
        'sql_mode' => 'custom',
        'sql' => 'SELECT `beispiel_id`, `bezeichnung`, `created` FROM `beispiel`',
        'base_table' => '',
        'base_alias' => 'rpt',
        'where' => '',
        'order_by' => '`beispiel_id` DESC',
    ],
    'columns' => [
        ['field' => 'beispiel_id', 'label' => 'ID', 'visible' => true, 'type' => 'number', 'align' => 'right'],
        ['field' => 'bezeichnung', 'label' => 'Bezeichnung', 'visible' => true, 'type' => 'text', 'align' => 'left'],
        ['field' => 'created', 'label' => 'Erstellt', 'visible' => true, 'type' => 'datetime', 'align' => 'left'],
    ],
    'filters' => [
        ['alias' => 'bezeichnung', 'label' => 'Bezeichnung', 'enabled' => true, 'type' => 'text', 'operators' => ['contains', '=', 'starts_with'], 'default_operator' => 'contains'],
    ],
    'summary' => [
        ['label' => 'Anzahl', 'type' => 'count', 'field' => '*'],
    ],
    'ui' => [
        'headline' => 'Beispielreport',
        'intro' => 'Filterbarer Bericht mit Export',
        'show_filter_bar' => true,
        'show_summary' => true,
        'striped_rows' => true,
        'empty_message' => 'Keine Daten gefunden.',
    ],
    'exports' => [
        'pdf' => true,
        'xls' => true,
        'csv' => true,
        'json' => true,
        'default_filename' => 'beispielreport',
        'pdf_orientation' => 'L',
        'pdf_page_size' => 'A4',
    ],
];
```

## 20. Minimalvorlage `report_function.php`

```php
<?php

function onBeforeQuery(array &$context, array &$queryPayload, array &$reportConfig): void
{
    // Optional: WHERE-Klauseln oder Session-Parameter vorbereiten
}

function onAfterQuery(array &$context, array &$rows, array &$reportConfig): void
{
    foreach ($rows as &$row) {
        $row['bezeichnung'] = trim((string)($row['bezeichnung'] ?? ''));
    }
}
```

## 21. Vorlage fuer Auditfelder im Grid nicht anzeigen

Wenn Auditfelder fachlich nur in der Maske sichtbar sein sollen:

```php
// Im grid.cfg nicht als sichtbare Spalten aufnehmen
```

Im Formular:

```php
'TAB_SYSTEM' => [
    'label' => 'System',
    'fields' => [
        'created' => [
            'label' => 'Erstellt am',
            'type' => 'datetime',
            'hidden_on_insert' => true,
            'readonly_on_insert' => true,
            'readonly_on_update' => true,
        ],
        'updated' => [
            'label' => 'Geaendert am',
            'type' => 'datetime',
            'hidden_on_insert' => true,
            'readonly_on_insert' => true,
            'readonly_on_update' => true,
        ],
        'created_by' => [
            'label' => 'Erstellt von',
            'type' => 'select',
            'hidden_on_insert' => true,
            'readonly_on_insert' => true,
            'readonly_on_update' => true,
        ],
        'updated_by' => [
            'label' => 'Geaendert von',
            'type' => 'select',
            'hidden_on_insert' => true,
            'readonly_on_insert' => true,
            'readonly_on_update' => true,
        ],
    ],
],
```

## 22. Vorlage fuer Dateiupload-Modul

```php
'anlage_datei' => [
    'label' => 'Anlage',
    'type' => 'file_upload_to_path',
    'upload_path' => '_file_store/berichte_formulare',
],
```

Regeln:

- Dateilogik moeglichst in der Modul-`_function.php`
- spezielle Bildvorschau oder Downloadlogik nicht automatisch global in `_pgm` loesen

## 23. Kurz-Checkliste fuer KI

Vor Abgabe pruefen:

1. Stimmen Runner und Dateinamen?
2. Ist `primary_key` ein Array?
3. Sind Untertabellen sauber auf Elternkontext gefiltert?
4. Sind Delete-Sperren serverseitig abgesichert?
5. Liegen Auditfelder im `System`-Tab?
6. Wurde `transaction` nur bewusst gesetzt?
7. Wurden keine Legacy-Runner verwendet?

## 24. Empfohlene Arbeitsreihenfolge

1. SQL oder Tabelle bauen
2. `grid.cfg`
3. `form.cfg`
4. Hook-Datei
5. Menue
6. Filter
7. Delete-Schutz
8. Syntaxcheck

## 25. Merksatz

Wenn eine KI in MOSAIC schnell und sauber sein soll:

- zuerst Konfiguration
- dann kleine Hooks
- Kern nur fuer echte Rahmenregeln

## 26. Minimalvorlage `forum.cfg`

```php
<?php

return [
    'title' => 'MOSAIC Forum',
    'project' => 'mo_user',
    'database' => 'mo_user',
    'module' => 'main',
    'module_path' => 'forum',
    'config_relative_path' => 'forum/main_forum.cfg',
    'bootstrap_file' => 'forum/forum_lib.php',
    'assets_relative_path' => 'forum/assets',
    'storage_relative_path' => 'forum/forum_anlagen',
    'setup_sql_relative_path' => 'forum/setup_forum.sql',
    'menu' => [
        'runner' => 'show_menu.php',
        'module' => 'main',
        'module_path' => 'forum/menu',
    ],
    'routing' => [
        'default_view' => 'start',
    ],
    'views' => [
        'start' => [
            'label' => 'Start',
            'script' => 'start.php',
        ],
        'overview' => [
            'label' => 'Uebersicht',
            'script' => 'index.php',
        ],
        'board' => [
            'label' => 'Board',
            'script' => 'board.php',
        ],
        'topic' => [
            'label' => 'Thema',
            'script' => 'topic.php',
        ],
        'topic_create' => [
            'label' => 'Neues Thema',
            'script' => 'topic_create.php',
        ],
        'action' => [
            'label' => 'Aktion',
            'script' => 'topic_action.php',
        ],
        'attachment' => [
            'label' => 'Anlage',
            'script' => 'attachment_download.php',
        ],
    ],
];
```

Regeln:

- Forum-Links immer ueber `show_forum.php`
- interner Aufruf ueber `view`
- `bootstrap_file` laedt die projektspezifische Forum-Bibliothek
- `setup_sql_relative_path` enthaelt Tabellen und Default-Inhalte
- Dateianlagen liegen physisch nur unter `storage_relative_path`
