Files
AgrarmonitorConnector/README.md
T
bjoernpoettkerandClaude Opus 5.5 659fdb5293 Arbeitszeit: Arbeitstag beginnen, beenden, aendern und loeschen
beginneArbeitstag legt per saveSchicht.php eine laufende Schicht fuer
beliebige Mitarbeiter an (der Menuepunkt "Arbeitstag beginnen" wirkt nur
fuer den angemeldeten Benutzer). Damit kann auch eine Pause die erste
Buchung des Tages sein. Zeitbuchungen lassen laufende Schichten offen
und ziehen nur den Beginn bei Bedarf vor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:54:58 +02:00

115 lines
8.3 KiB
Markdown

# Agrarmonitor Connector
TypeScript MVP connector for Agrarmonitor with shared cookie persistence, optional AES-GCM cookie encryption, automatic login, retry after expired sessions, device registration checks, and customer detail extraction.
Login uses Agrarmonitor's redirect flow only: the connector loads `/`, extracts the `nonce`, and posts credentials to `/redirect.php?id=benutzerverwaltung&action=login`.
## Installation
```bash
npm install
```
## Build
```bash
npm run build
```
## Example
```ts
import {
AesGcmCookieEncryptor,
FileCookieStore,
createAgrarmonitorClient,
} from 'agrarmonitor-connector';
const agrarmonitor = await createAgrarmonitorClient({
baseUrl: 'https://admin7.agrarmonitor.de',
apiToken: process.env.AGRARMONITOR_API_TOKEN,
username: process.env.AGRARMONITOR_USERNAME ?? '',
password: process.env.AGRARMONITOR_PASSWORD ?? '',
cookieStore: new FileCookieStore(
process.env.AGRARMONITOR_COOKIE_PATH ?? './data/agrarmonitor-cookies.json',
{
encryptor: process.env.AGRARMONITOR_ENCRYPTION_KEY
? new AesGcmCookieEncryptor(process.env.AGRARMONITOR_ENCRYPTION_KEY)
: undefined,
logger: console,
}
),
logger: console,
});
const freischaltung = await agrarmonitor.checkFreigeschaltet();
console.log(freischaltung.freigeschaltet);
const registrierung = await agrarmonitor.checkRegistriert();
console.log(registrierung.registriert);
const response = await agrarmonitor.http.get('/kunden/detail/123');
console.log(response.status);
```
## Login-Logging
Der Login-Flow protokolliert jeden Schritt auf `debug`-Level, sofern der uebergebene Logger eine `debug`-Methode hat. Ohne `debug`-Methode bleiben diese Details still - ein reiner `info`-Logger sieht davon nichts.
Protokolliert werden der Aufruf der Loginseite, die gelesene Nonce, der `POST /login/api/login.php`, die Session-Pruefung und jeder Request, der wegen abgelaufener Session wiederholt wird. Zu jedem dieser Requests steht ein Cookie-Snapshot **vor** dem Absenden und einer **danach** im Log, dazu ein Delta mit neuen, geaenderten und entfernten Cookies:
```
[debug] Login-POST: POST /login/api/login.php
{"phase":"request","method":"POST","url":"/login/api/login.php","cookies":[],
"body":{"username":"demo","passwort":"***","nonce":"9f3a1c7b2e...","ssoAction":"","ssoReturn":""}}
[debug] Login-POST: POST /login/api/login.php -> 200
{"phase":"response","status":200,"istLoginSeite":false,
"setCookie":["PHPSESSID=f81d…(len=32); Path=/; HttpOnly"],
"cookies":[{"name":"PHPSESSID","value":"f81d…(len=32)","domain":"127.0.0.1","path":"/",
"expires":"Session","httpOnly":true,"secure":false}],
"cookieChanges":{"added":["PHPSESSID"],"changed":[],"removed":[]}}
```
Passwort und Cookie-Werte stehen nie im Klartext im Log: das Passwort wird durch `***` ersetzt, Cookie-Werte und der rohe `Set-Cookie`-Header werden auf die ersten vier Zeichen plus Laenge gekuerzt. Damit bleibt erkennbar, ob sich eine Session-ID geaendert hat, ohne dass das Log selbst die Session preisgibt.
Das `phase`-Feld ordnet jeden Eintrag zu: `login-start`, `request`, `response`, `nonce`, `session-check`, `session-expired`, `retry`, `error`, `login-done`.
## Cookie Persistence
`FileCookieStore` keeps one shared `CookieJar` per file path inside the Node process. Multiple connector instances that use the same cookie file therefore reuse the same session and every successful request saves the latest cookies back to disk.
The store can read both the connector format and the older Telefonbuch cookie-array format.
## Useful Methods
- `checkFreigeschaltet()` checks whether Agrarmonitor redirects to `/freischaltung/`.
- `checkRegistriert()` checks whether the page still contains `Neues Gerät registrieren`.
- `registerDevice({ agrarmonitorId, pcName })` loads `/freischaltung/`, extracts the nonce, and posts the registration request.
- `fetchCustomers()` loads customers from `https://api.agrarmonitor.de/v1/kunden`.
- `getKunden2()` loads all customer pages and returns normalized customer/supplier fields.
- `getCustomerById(id)` loads one customer from the token-based Agrarmonitor API.
- `eingangsrechnungenLivesearch(suchstring)` searches invoice files and enriches matching Eingangsrechnungen with edit/detail metadata.
- `eingangsrechnungVorhanden(suchstring)` checks whether invoice file search has rows.
- `eingangsrechnungImDateieingangVorhanden(suchstring)` checks whether file inbox search has rows.
- `getRechnungsdaten(rechnungId)` reads editable invoice fields.
- `setRechnungsdaten(rechnungId, daten)` updates editable invoice fields.
- `setLieferscheinNummer(rechnungId, nummer)` updates only the delivery note number.
- `setEingangsdatum(rechnungId, datum)` updates the received date.
- `getMaschinenKategorien()` reads `/maschinen/kategorien` and returns ID, name, tractor flag, and mobile visibility.
- `getMaschinen(gruppenId, suchstring?)` reads `/module/maschinen/livesearch.php` and returns ID, number, name, license plate, and active flag.
- `getAlleMaschinen()` reads all machine categories (`maschinengruppe=0`), active and inactive machines, all livesearch pages, and returns each machine once with ID, number, name, license plate, active flag, its first category (`gruppenId`, `gruppe`) and all categories (`gruppenIds`); a machine can be listed in several categories. `getMaschinen()` already sends `maschinen_aktiv=1`, which in Agrarmonitor means active **and** inactive.
- `getFirmen()` reads `/einstellungen` and returns company ID and description.
- `getArtikelEinheiten()` reads `/artikel/einheiten` and returns unit ID, description, and short label.
- `getArtikelKategorien()` reads `/artikel/kategorien` and returns product group ID and name.
- `getArtikel(artikelGruppe, suchstring?)` reads `/module/artikel/livesearch.php` and returns article ID, number, and description.
- `getMitarbeiter(suchstring?, mitarbeitergruppe?)` reads `/module/mitarbeiter/livesearch.php` and returns employee ID, number, last name, first name, and active flag.
- `getTaetigkeiten()` reads `/module/arbeitszeit/taetigkeiten-livesearch.php` (active and inactive) and returns activity ID, UUID, number, name, active flag, area (`bereichId`, `bereich`) and the flags `maschineZuordnen`, `abwesenheit`, `notizErzwingen`.
- Agrarmonitor has no cost center master data (`Kostenstelle` is only a free-text field on machines, employees and articles), so there is no `getKostenstellen()`. See `docs/zeitbuchung-analyse.md`.
- `beginneArbeitstag(mitarbeiterId, start, { datum? })` starts the workday: creates a running shift (attendance without end) for any employee and returns `{ ref }` (`schicht:<mitarbeiterId>:<YYYY-MM-DD>:<id>`). If a running shift already exists that day, its ref is returned instead. The Agrarmonitor menu link "Arbeitstag beginnen" (`bueroAnundAbmelden.php`) only works for the logged-in user, so the connector uses `module/auftraege/api/saveSchicht.php`.
- `beendeArbeitstag(ref, ende)` ends the shift, `updateArbeitstag(ref, { start, ende })` corrects it (`ende: null` keeps it running), `deleteArbeitstag(ref)` deletes the shift including all activities and breaks.
- `createZeitbuchung(daten)` books an activity block (`taetigkeitId`, optional `maschinenId`, `notiz`, `kostenstelle`) or a break (`art: 'pause'`) for `mitarbeiterId` from `start` to `ende` (minute precision, Europe/Berlin) and returns `{ ref }`. Bookings go into the shift of that day (started with `beginneArbeitstag`), which is extended if needed; a running shift stays open. Without any shift, an activity creates one via `nachtragen.php`, a break is rejected. Agrarmonitor has no cost center field, so `kostenstelle` is appended to the note as `Kostenstelle: …`. Pass `datum` (YYYY-MM-DD) for blocks after midnight that belong to the previous workday.
- `updateZeitbuchung(ref, daten)` changes times, activity, machine and note of a booking (the old note is replaced); changing the type (activity/break), employee or workday is rejected.
- `deleteZeitbuchung(ref)` deletes a booking; deleting the last activity of a shift also deletes the shift in Agrarmonitor.
- Refs look like `verteilung:<mitarbeiterId>:<YYYY-MM-DD>:<id>` or `pause:…`. Rejections by Agrarmonitor (HTTP 4xx such as 422 with the reason in the body) and invalid input throw `ZeitbuchungError` (`grund`, `status`); network and 5xx errors are thrown unchanged so callers can retry.
- `saveSession()` explicitly persists the current cookie jar.