Developer Documentation

What DiPAgE stores, what it will accept from you, and what it talks to. Written for people building importers, exporters and integrations.

Angular 22 · zoneless IndexedDB · no backend PWA · offline-first

Press / to search · Esc to clear

If you need help using the app, then go to the user documentation

Looking for the data standard or sample exports? Go to downloads

Key developer resources

Record field reference

Every field of the PSM-Anwendungsdatensatz — 69 of them — with type, constraints, example and the behaviour that is not visible in the schema.

Open the field reference
Data standard & example files

The JSON Schema, an XSD variant, and example exports in CSV, JSON and XML — all in one place.

Go to downloads
Looking for the user guide?

Installing the app, recording an application, exporting for an audit — the task-oriented documentation lives on its own page.

Go to the user docs
Start here

Developer questions.

The questions that come up when someone first tries to integrate with DiPAgE, roughly in the order they come up.

9 questions

No. DiPAgE has no backend of its own and no server-side database — there is nothing to authenticate against and no endpoint that returns your records over the network. Every record lives in IndexedDB in the browser that entered it. Integration from outside the browser happens through files: export from the app, import into the app. The formats are documented under Import, export and interchange, and the record shape has its own reference page. Code running inside the page is a different matter — see the in-page JavaScript API in the next entry.

Yes — that is what window.dipageApi exists for. The app freezes a small object onto window at startup (currently version 1.1.0) so code running in the page can read and write records through methods instead of clicking through the interface, which is the intended path for an agent gathering or entering data. It exposes createRecord, openNewRecord, updateRecord, deleteRecord, getRecord, getRecords, searchPsm, searchCrops, getProfile, exportRecords and getTemplates. Calls go through the same storage layer as the user interface, and therefore the same schema sanitizer, so the API cannot write a record shape the form could not produce. It is an in-page surface only: no network endpoint sits behind it, it exists solely in a browser tab with DiPAgE open, and it disappears with that tab.

Validation boundaries for geographic coordinates (WGS84) ensure that recorded coordinates fall within mathematically and geographically valid ranges and reject invalid inputs. In the data model, these limits are maintained synchronously in both the JSON Schema and the TypeScript validation logic. When customizing boundaries in the codebase, both locations should be updated in the same change to maintain consistent validation across all interfaces.

Because the standardized record format captures emergency authorizations through application notes, DiPAgE records standardized, language-independent identifier codes in the notizen field (e.g. PPS_EA, PPS_NZ, ŚOR_ZN). When records are read or edited, this identifier is recognized automatically, ensuring classification is reliably preserved across export and re-import cycles. External tools and parsers are advised to preserve existing contents in notizen. In addition, note that emergency authorizations do not carry a regular registration number under authorization regulations.

In the PSM data standard, every applied product is recorded as an individual record to precisely satisfy statutory documentation requirements for each separate plant protection product and its specific application rate. To represent applications of multiple products in a single operation, DiPAgE links these records through coherent context parameters: treatment location, application date, and a coordinated creation timestamp. This ensures full compliance for each single product record while allowing the user interface to display and manage the entries as a cohesive tank mix.

Two different things, neither of them a schema field. A draft is a record the user saved with soft validation; it carries status: "draft" in IndexedDB, but the schema deliberately excludes status — the sanitizer strips it on every read and write, and the storage layer re-attaches it afterwards so a draft survives a reload. status never appears in an export. Incomplete is not stored at all — it is computed by re-checking a saved record for missing required fields. The export gate refuses incomplete records and drafts with a notification; full backups keep both. The UI-only product fields indikation, anwendungszeitpunkt and notfallzulassung are stripped by the sanitizer as well.

For JSON, the contract is the schema — validate against it and you are done. For the tabular formats there is one more rule worth knowing: CSV and the Excel workbook both carry a two-row header. Row 1 holds localized labels for humans, row 2 holds the machine field paths (anwendung_zeitpunkt.datum and so on), and data starts at row 3. The importer discovers the path row rather than assuming a position, so a file exported in German re-imports correctly into an English interface. Write row 2 correctly and the labels in row 1 can say anything you like.

Only lookups, and only to the origins pinned in its Content-Security-Policy: synops.julius-kuehn.de for products, indications, crops and emergency authorizations; nominatim.openstreetmap.org for postal-code geocoding; and the JKI GeoServer plus OSM/Esri tile hosts for the map and field-geometry pickers. Those origins are pinned in the Content-Security-Policy, so nothing else can be contacted even if a dependency tried. There is no telemetry and no analytics. Your records never leave the device unless you export them yourself.

DiPAgE’s .xml export is structured as an XML SpreadsheetML 2003 workbook. This XML-based tabular format allows documented applications to be opened directly and natively in spreadsheet software such as Microsoft Excel, featuring styled headers and an embedded reference sheet of permissible unit codes. DiPAgE can also re-import these files seamlessly, making the format an ideal offline template and operational data exchange format.

Technical topics.

Each topic is self-contained. Open the one you need; the rest stay collapsed.

The shape of the thing

DiPAgE is a single-page Angular application that runs entirely in the browser. There is no application server, no database to provision and no account system. The only server involved is the one serving the static bundle, and its job ends there. Everything the app knows lives in IndexedDB on the device that entered it.

Framework choices worth knowing

  • Angular 22 with standalone components and signals throughout. Change detection is zoneless and every component is OnPush.
  • TypeScript strict mode, including strictTemplates. Note the gap this leaves: tsc does not check templates, so a stale binding surfaces only at AOT build time.
  • Tailwind CSS 4 with DaisyUI 5. Custom CSS exists only where a utility cannot express the rule.
  • UUIDv7 for every generated ID, so identifiers sort by creation time.

What zoneless costs you

If you write tests against this codebase, the one trap worth naming up front: under zoneless change detection a debounceTime or delay subscription created at component construction cannot be flushed by tick() inside fakeAsync. The assertion then passes without exercising the debounce at all. Create the subscription inside the test body instead.

Local data storage and data sovereignty

No sync, no multi-device merge, no server-side backup. Moving data between devices is an explicit export and import, and the conflict resolution that goes with it is a user-facing flow rather than an algorithm running behind the scenes. That is a deliberate trade for working with no connectivity and holding no personal data centrally.

The unit of storage

The stored entity is the PSM-Anwendungsdatensatz, identified as urn:psm:anwendung-datensatz:1.0.0 and defined in record.schema.json (JSON Schema draft 2020-12). One record describes one product applied once. Several products in one spray pass, or several treated parts, become several records — the form multiplies them out on save.

Where the complexity actually is

Most of the schema is flat and unsurprising. The exception is behandlungsort, an array whose standort_kennung_wert is a seven-way union discriminated by standort_kennung_art: coordinates, cadastral parcel, InVeKoS reference, field geometry, railway section, forestry address, or road. One of those — roads — nests a further union inside itself. The schema expresses this as chains of if/then under allOf rather than as named variants, so a generic validator handles it but hand-written validation has to reproduce the branching.

Two rules that are not in the schema

  1. The sanitizer is the shape gate. Every read from and write to storage is whitelisted against the schema. A field the schema does not know about is silently dropped — not rejected loudly. If a field you wrote is missing afterwards, this is why.
  2. Some fields exist on the model but never appear in an export. The product’s indikation, anwendungszeitpunkt and notfallzulassung are UI-only and are not stored either. The exception is the record’s status: the sanitizer strips it, but the storage layer re-attaches draft/saved afterwards so a draft survives a reload.

Field-by-field

Every field, its type, its constraints, a pasteable example and the behaviour that is not visible in the schema are documented on a page of their own, along with a downloadable copy of the schema and a complete example record covering all seven location variants.

Layout

Everything lives in a single IndexedDB database, psm-application-db, currently at DB_VERSION 13. Six object stores: records (application records), partial_data (reusable master-data templates, indexed by type), settings, crops (the synced crop list), notfallzulassungen (synced emergency authorizations) and logs.

The sanitizer sits on every path

Reads and writes both pass through the schema whitelist. That is what keeps a foreign or stale field from reaching storage, and it is also why writing to IndexedDB by hand is not a supported integration route — your extra keys will not survive the next read. For debugging you can still open the database in any browser devtools panel (store records) — as a read-only diagnostic.

Persistence and browser storage management

On startup, the application requests persistent storage via navigator.storage.persist(). Because final storage retention is governed by the policies of the respective browser and operating system (especially under device storage constraints), users should regularly create backups using the built-in backup function (ZIP export). DiPAgE does not provide cloud synchronization or automated central backup services; responsibility for retaining statutory documentation remains with users and agricultural enterprises.

If you change the schema

There are three independent version numbers that must not be confused: datensatz_format_version is frozen at "1.0.0" as a const in the schema and describes the record format; an informal counter ("v12", "v13") in code comments and the changelog tracks schema revision passes and is not stored in any record; DB_VERSION is the IndexedDB upgrade version and never appears in an exported file. A schema change does not necessarily move DB_VERSION. The version numbers are coupled to places you will not find by running the test suite: the end-to-end helpers and the schema-integrity spec both hardcode DB_VERSION. Moving it in one place and not the others produces a few hundred failures that read like flakiness rather than like a version mismatch.

Export formats

  • JSON — pretty-printed records, exactly the schema shape. The format to build against.
  • XML — structured as a SpreadsheetML 2003 workbook that spreadsheet software such as Excel opens natively: formatted headers, frozen header row, and an embedded reference sheet with all 284 permissible unit codes. Well-suited as an offline editable template and for seamless re-import.
  • CSV — flattened, BOM-prefixed, with formula-looking values neutralised against CSV injection.
  • PDF — a sectioned, audit-oriented layout carrying the operator imprint. Field geometry is summarised here as type, vertex count and centroid rather than raw coordinates; every other format keeps the full array.

The two-row header

CSV and the workbook share one contract worth understanding before you write a parser. Row 1 holds localized human labels. Row 2 holds machine field paths — anwendung_zeitpunkt.datum, behandlungsort[0].bezeichnung. Data starts at row 3. The importer locates the path row rather than assuming its position, and also handles a legacy single-header file. This is what lets a file exported from the German interface import correctly into the English one.

Import

Accepted: .json, .csv, .xml, and .zip containing those. Imports are normalised (dot-path unflattening, per-field type coercion, German decimal commas and DD.MM.YYYY dates) and then checked against the schema. Records missing required fields go to a review step where the user decides whether to import them anyway; structurally unusable entries are counted and reported. The three system fields are filled during normalisation when absent, so a hand-filled template with a blank ID column still imports.

Conflicts

A same-ID import with differing content opens a per-record version picker — imported version against current version, with a "keep both" option. A content-fingerprint match with differing IDs is treated as its own conflict class, which is what catches the same template imported twice. The fingerprint is stable across key order, ignores the ID, creation timestamp, notes and status, and is sensitive to product, rate, date, crop, location and operators.

Backups

A full backup is a ZIP containing records.json, master_data.json, profiles.json, settings.json and metadata.json. Restore is deliberately staged: an explicit confirmation first, then parsing with no writes, then conflict classification, then commit. Records are restored as they were saved — drafts and incomplete records included — because schema compliance is enforced at export, not at restore. If writes fail midway the app reports a partial restore rather than inviting a retry that would double-apply.

Two layers of validation

The first layer is the JSON schema: required fields, types, enums, formats, additionalProperties: false everywhere, and the conditional narrowings expressed as if/then chains. The second layer is application code, which enforces domain-specific operational and statutory consistency rules.

Rules that live only in application code

  • Geometry nesting depth must match the geometry type. The schema only requires that koordinaten is an array.
  • Rate must be greater than zero. The schema says number.
  • Application time becomes mandatory when the selected product carries a bee-hazard classification.
  • Per-location-type field checks beyond what the if/then chain covers.

Formatting time and numeric values

When entering and validating time values (anwendung_zeitpunkt.uhrzeit), DiPAgE supports both full timestamps with timezone offsets and simplified local time notation (HH:mm:ss) to ensure practical flexibility during field documentation. For floating-point decimal values (such as railway kilometer markers), external validation tools should apply standard floating-point precision tolerances.

Hard versus soft

Not every rule blocks. Master-data templates use soft validation throughout: warnings never prevent a save. In the record form, several cross-field checks are advisory only — a harvest year that disagrees with the application date, a treated size larger than the site, a reversed BBCH range. They surface as warnings and let the save through.

Incomplete records

"Incomplete" is computed, not stored: a saved record is re-checked for missing required fields and flagged with a list of what is missing. Incomplete records are pinned to the top of the list and refused by the export gate with a notification, but they are kept in full backups so a restore is lossless.

SynOPS — the substantive one

synops.julius-kuehn.de provides four things: product search by trade name or authorization number, scoped to the selected crop and grouped by approval status; indications for a product/crop pair, including hazard codes and dosing suggestions that auto-fill the rate; the crop list, synced per language into IndexedDB so lookup works offline; and emergency authorizations, likewise synced locally. Product and indication lookups are live and debounced, with stale responses cancelled. Crops and emergency authorizations are synced datasets with a staleness check rather than per-request fetches.

Geocoding and map data

nominatim.openstreetmap.org resolves postal codes for the map picker. The JKI GeoServer serves the field-geometry picker over WFS: official field blocks with FLIK and plot numbers for North Rhine-Westphalia (inv:NRW_FB_<year>), and crop-classification outlines without any field identity for the rest of Germany (cora:CORA_<year>); OSM and Esri hosts serve the tiles. After picking a polygon, the user ticks which extra identity entries (InVeKoS reference and/or centroid coordinates) are added — nothing is preselected. They are linked to the polygon only by a shared bezeichnung, since the schema has no link field.

All of it is pinned

Every one of those origins is listed in the Content-Security-Policy. Nothing else can be contacted, which is the mechanism behind the "no telemetry" claim rather than a promise.

Degradation

Failures and offline state flip a global banner between offline and degraded. Nothing local is affected: every field can still be filled in by hand, records still save, exports still run. Only the lookups go quiet.

If you are integrating

These are the app’s upstreams, not an API surface DiPAgE offers you. There is no DiPAgE endpoint to call — see the first FAQ entry.

Caching model

Offline capability comes from the Angular service worker, configured in ngsw-config.json. The app shell and assets are precached; the external API origins get their own data groups. After the first successful load the app is fully operable with no network — record entry, validation, storage, export and PDF generation all run locally.

Synced datasets versus live lookups

Two different mechanisms, easy to conflate. Crops, emergency authorizations and bee-hazard codes are synced into IndexedDB and read from there, with a staleness check on boot rather than a fetch per query — which is why crop autocomplete works with no signal. Product search and indications are live and degrade when offline.

Map tiles bypass the service worker

Every map tile URL carries a constant ngsw-bypass parameter, so tiles are not routed through the service worker at all, and tile loads retry through OpenLayers’ own re-entry point rather than through the cache layer. Worth knowing if you are debugging why tiles behave differently from every other request.

Install and detection

The app is installable as a PWA and detects standalone mode. Installability varies by platform in ways that are a support question rather than a code question — the user documentation covers which browsers and operating systems allow it.

Where the risk actually is

There is no server, no session and no multi-tenancy, so most of the usual web attack surface is simply absent. What remains is that the app parses files the user hands it — JSON, CSV, XML, ZIP — from sources it cannot vouch for. That is the boundary the hardening is built around.

Import hardening

  • ZIP: entry-name whitelist, size and count limits, per-entry decompressed-size cap. Full backup archives are structurally rejected by the record importer before any entry is parsed.
  • XML: <!DOCTYPE> rejected outright, with a 2 MB cap.
  • Prototype pollution: every iteration over untrusted keys is guarded. This matters more than usual here, because dot-path unflattening walks attacker-supplied key paths by construction.
  • Backup restore: records are restored as they were saved (only entries that are not objects are skipped) and pass through the sanitizer on every read; templates are shape-validated and profiles field-whitelisted. Anything rejected is counted and reported rather than silently dropped.

Export hardening

CSV injection is neutralised on export: a value that looks like a formula is escaped so it cannot execute when the file is opened in a spreadsheet. Formula-looking values are still preserved verbatim in the workbook path, so the round trip does not corrupt data.

Content-Security-Policy

The CSP pins the external origins listed under external APIs. It is defined in the index.html meta tag and in the response header of whatever serves the app — the two are enforced separately and must agree; frame-ancestors is header-only, since a meta CSP cannot express it. Alongside it: X-Frame-Options, nosniff, a referrer policy, and a path-traversal guard in the static server.

Consent before storage

The router outlet renders only after storage consent is given, so nothing is written before the user agrees. If you are writing tests that seed consent directly into IndexedDB, reload afterwards — the app does not re-read consent reactively, and skipping that reload has broken entire suites.

Where CSP and visual tests disagree

One trap worth naming: a CSP-blocked resource renders consistently broken, so a visual snapshot of it still matches a consistently broken baseline. A new external origin has to be verified in a served build; green end-to-end tests do not prove it works.

Translation

Three locales — de (default), en and pl — held at full key parity, with dot-notation keys and {param} interpolation. Locale-aware formatting covers dates, times and generated filenames. The language preference is stored in IndexedDB and is independent of the device language.

Two rules that shape the code

  1. No raw user-facing literals. Even error messages surface as translated keys; the detail goes to the log store instead.
  2. translate() returns the key on a miss, never a falsy value. So a missing translation shows up as a visible key rather than an empty element — deliberate, because an empty label is much easier to miss in review.

Accessibility

The target is WCAG 2.2, enforced from three directions: ESLint accessibility rules on templates, axe-core end-to-end runs in both themes, and a keyboard tab-walk test. Screen-reader announcements go through two ARIA live regions, one polite and one assertive, owned by a single service. Route changes, theme, language and font-scale switches, and modal appearances all announce.

Theming and scaling

Two themes, jkiLight and jkiDark, applied via data-theme and persisted. Font scale is 100, 150 or 200 percent, applied at the root font size, also persisted. If you add a component, it has to hold up at 200 percent in both themes — that combination is where layout breaks first.

The layers

  • Lint — style and template accessibility rules.
  • Type-check — TypeScript in .ts files. Not templates.
  • Invariants — a cross-file consistency script: the DB_VERSION triple, the CSP meta/header pair, the service-worker data groups, the geo bounds in TypeScript against the schema, the notizen max length, the generated schema validator being in sync, additionalProperties: false on every object, i18n parity and orphan keys, and a minimum release age for new pnpm packages.
  • Unit — over 3,400 Karma/Jasmine specs, around 92% statement and 85% branch coverage. No threshold is configured, so these are a measurement rather than a gate.
  • Build (AOT) — the only gate that catches a stale template binding.
  • End-to-end — real flows plus visual snapshots across desktop, mobile Chrome and mobile Safari.
  • Accessibility — axe-core in both themes, plus a keyboard tab-walk.
  • Server security — path-traversal rejection and security headers.

Where a green run lies

Worth internalising before you trust a passing pipeline:

  1. A green type-check does not mean a rename landed — templates are only checked at AOT build.
  2. Compiler-silenced fixtures (as unknown as casts on record shapes) keep passing against a stale schema after a field change.
  3. A spy list missing a newly added method returns undefined and produces a misleading "never called" failure.
  4. A CSP-blocked resource still matches its broken baseline, so visual tests do not prove a new origin works.

Snapshot policy

Visual baselines are updated by a human, never automatically and never by CI. The diff is the review signal; regenerating it on failure throws away the only thing the test was measuring.

Looking for a specific field? Open the record field reference

Get started immediately

Use DiPAgE directly in your browser. No registration and free of charge.

Open the application and start documenting your plant protection product applications right away. No installation is required, but on supported devices DiPAgE can additionally be installed and used offline.

Directly in browser
No installation required. Open the application and get started immediately.
No registration
DiPAgE can be used without a user account or registration.
Stored locally
Your documentation data remains on the device you use.
Usable offline
On supported devices, DiPAgE can be installed and used without an internet connection.