fix: AJAX-URL dynamisch aus Script-Pfad ermitteln

urlRoot wird einmalig aus dem src-Attribut des globalnotify-Scripts
abgeleitet, damit AJAX-Calls unabhängig vom Installationspfad
funktionieren (z.B. /dolibarr/custom/... statt /custom/...).
Behebt 404-Fehler bei Aktionen wie "Alle als gelesen markieren".

Zusätzlich CLAUDE.md Projektdokumentation erstellt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-08 09:21:53 +01:00
parent b4a6f534ba
commit e67b094466
2 changed files with 133 additions and 1 deletions

124
CLAUDE.md Normal file
View file

@ -0,0 +1,124 @@
# GlobalNotify - Dolibarr Custom Module
Globales Benachrichtigungssystem als schwebendes Widget (FAB) in der unteren linken Ecke.
Sammelt Alerts von allen Modulen (Cron-Fehler, Warnungen, Aktionen) und zeigt sie einheitlich an.
## Modul-Metadaten
- **Modul-Nummer**: 500100
- **Version**: 1.4.0
- **Autor**: Eduard Wisch / Data IT Solution
- **Hooks**: `main`, `toprightmenu`
- **Nur für Admins** (`$user->admin`)
## Projektstruktur
```
globalnotify/
admin/setup.php # Admin-Seite: Einstellungen + Übersicht
ajax/action.php # AJAX-Endpunkt für JS-Aktionen
class/
globalnotify.class.php # Kern-Klasse: Notification CRUD
actions_globalnotify.class.php # Hook-Klasse: Widget-Rendering + Cron-Checks
core/modules/modGlobalNotify.class.php # Modul-Deskriptor
css/globalnotify.css # Widget-Styles (Dark Mode Support)
js/globalnotify.js # Widget-Logik (Drag, AJAX, Sound)
langs/de_DE/globalnotify.lang # Deutsche Übersetzungen
langs/en_US/globalnotify.lang # Englische Übersetzungen
```
## Architektur
### Speicherung
Notifications werden als JSON-Arrays in `llx_const` gespeichert (NICHT in eigenen Tabellen):
- Schlüssel: `GLOBALNOTIFY_{MODULE}` (z.B. `GLOBALNOTIFY_CRON`, `GLOBALNOTIFY_BANKIMPORT`)
- Wert: JSON-Array mit Notification-Objekten
- Max 50 Notifications pro Modul
- Interne Settings: `GLOBALNOTIFY_CRON_LASTCHECK`, `GLOBALNOTIFY_CRON_CHECK_INTERVAL`
### Notification-Typen (Konstanten in GlobalNotify)
- `TYPE_ERROR` = `'error'` — Priorität 10
- `TYPE_ACTION` = `'action'` — Priorität 9
- `TYPE_WARNING` = `'warning'` — Priorität 7
- `TYPE_INFO` = `'info'` — Priorität 3
- `TYPE_SUCCESS` = `'success'`
### Notification-Objekt (JSON-Struktur)
```json
{
"id": "module_uniqid",
"module": "cron",
"type": "error",
"title": "Kurztitel",
"message": "Detailbeschreibung",
"action_url": "/cron/list.php",
"action_label": "Button-Text",
"priority": 10,
"user_id": 0,
"created": 1709000000,
"read": false
}
```
## API: Notifications senden (für andere Module)
```php
// Statische Helper (empfohlen):
dol_include_once('/globalnotify/class/globalnotify.class.php');
GlobalNotify::error('meinmodul', 'Titel', 'Nachricht', $actionUrl, $actionLabel);
GlobalNotify::warning('meinmodul', 'Titel', 'Nachricht');
GlobalNotify::info('meinmodul', 'Titel', 'Nachricht');
GlobalNotify::actionRequired('meinmodul', 'Titel', 'Nachricht', $actionUrl);
// Oder manuell mit voller Kontrolle:
$notify = new GlobalNotify($db);
$notify->addNotification($module, $type, $title, $message, $actionUrl, $actionLabel, $priority, $userId);
```
## AJAX-Endpunkt (`ajax/action.php`)
POST-Requests mit `action` Parameter:
- `dismiss` — Einzelne Notification als gelesen markieren (`id` Parameter)
- `delete` — Notification löschen (`id` Parameter)
- `markallread` — Alle als gelesen markieren
- `getall` — Alle ungelesenen abrufen
- `getcount` — Ungelesene Anzahl abrufen
Authentifizierung: Nur `$user->admin`, CSRF-Token über `TOKEN` JS-Variable.
## JavaScript (globalnotify.js)
- **`GlobalNotify.urlRoot`**: Wird beim Laden einmalig aus dem Script-src-Pfad ermittelt (z.B. `/dolibarr`), damit AJAX-Calls unabhängig vom Installationspfad funktionieren
- **Drag & Drop**: FAB ist verschiebbar, Position wird in `localStorage` gespeichert
- **Click vs. Drag**: Erkennung über Distanz (<5px) und Zeit (<200ms)
- **Auto-Refresh**: Alle 2 Minuten via `getcount` AJAX-Call
- **Sound**: Web Audio API Doppel-Ton bei neuen Notifications
- **Animationen**: Shake + Bounce bei neuen Notifications
## Automatische Cron-Prüfungen (in Hook-Klasse)
1. **Hängende Jobs**: `processing=1` seit >30 Min → `TYPE_ERROR`
2. **Verpasste Jobs**: `datenextrun` >2 Std. in der Vergangenheit → `TYPE_WARNING`
3. **Cleanup**: Notifications für deaktivierte Cronjobs werden automatisch als gelesen markiert
4. **Cache**: Prüfintervall konfigurierbar via `GLOBALNOTIFY_CRON_CHECK_INTERVAL` (Default: 60s, Min: 10s, Max: 3600s)
## Modul-spezifische Checks
- **BankImport**: Prüft `BankImportCron::getCronStatus()` auf `paused`, `tan_required`, `login_error`, `fetch_error`, `session_expired`
- **ImportZugferd**: Prüft auf hängende `ImportZugferdScheduled` Cronjobs
## Admin-Seite (`admin/setup.php`)
- Einstellung: Cron-Prüfintervall
- Aktion: Alle Benachrichtigungen löschen (löscht alle `GLOBALNOTIFY_*` Konstanten)
- Aktion: Hängende Cron-Jobs zurücksetzen (`processing=0`)
- Tabelle: Cron-Job Übersicht mit Status
- Tabelle: Alle Benachrichtigungen mit Typ, Modul, Datum, Gelesen-Status
## Wichtige Konventionen
- Duplikat-Erkennung: Vor dem Erstellen wird geprüft, ob eine gleichartige ungelesene Notification existiert
- Prioritäts-Sortierung: Höchste Priorität + neuestes Datum zuerst
- `user_id=0` bedeutet: sichtbar für alle Admins
- Dark Mode wird via `prefers-color-scheme: dark` CSS Media Query unterstützt
- Keine eigenen DB-Tabellen, alles über `llx_const`

View file

@ -7,6 +7,14 @@ var GlobalNotify = {
isOpen: false,
isDragging: false,
dragOffset: { x: 0, y: 0 },
urlRoot: (function() {
var scripts = document.querySelectorAll('script[src*="globalnotify"]');
if (scripts.length > 0) {
var match = scripts[0].getAttribute('src').match(/^(.*?)\/custom\/globalnotify\//);
if (match) return match[1];
}
return '';
})(),
/**
* Toggle panel visibility
@ -208,7 +216,7 @@ var GlobalNotify = {
* AJAX helper
*/
ajaxCall: function(action, params, callback) {
var url = (typeof DOL_URL_ROOT !== 'undefined' ? DOL_URL_ROOT : '') + '/custom/globalnotify/ajax/action.php';
var url = this.urlRoot + '/custom/globalnotify/ajax/action.php';
var body = 'action=' + encodeURIComponent(action);
for (var key in params) {