ERNIE uses a Docker-first local workflow.
- Fast Mode is the default path for day-to-day development.
- Optional profiles are available for assessment-specific and parity-specific work.
- Canonical validation entry points remain
npm run check:backend,npm run check:frontend, andnpm run check:parity.
Host-side frontend commands require local node_modules in the repository checkout. Run npm ci after cloning and whenever package-lock.json changes. Use npm install only when intentionally adding or updating dependencies so npm can update the lockfile.
| Mode | Purpose | Command |
|---|---|---|
| Fast Mode | Start the core development stack only | npm run docker:dev:up |
| Assessment profile | Start F-UJI and the dedicated persistent assessment workers; also set FUJI_ENABLED=true in .env.docker |
npm run docker:dev:assessment |
| Parity profile | Start the parity stack including F-UJI and the dedicated persistent assessment workers; also set FUJI_ENABLED=true in .env.docker |
npm run docker:dev:parity |
Fast Mode is the default because it keeps the profile-gated F-UJI service out of the normal startup path.
WSL2 is the recommended Windows setup because Docker bind mounts and host-side Node tooling are significantly faster inside the WSL filesystem.
- Install Docker Desktop with WSL2 integration enabled.
- Clone the repository inside your WSL home directory, for example
~/src/ernie. - Open the project through VS Code Remote - WSL.
- Run Docker Compose and host-side Node commands from the WSL shell.
- Use your Windows browser for
https://ernie.localhost:3333if preferred.
The repository recommends Microsoft's TypeScript 7 extension for VS Code and enables its native language server with the workspace-local TypeScript 7 package. Accept the workspace extension recommendation after opening the checkout. Run npm ci first so the configured SDK path is available.
If the repository stays under D:\ or another NTFS path:
- expect slower bind-mount performance than WSL2
- keep
VITE_USE_POLLING=trueenabled - use the
public/hottroubleshooting step below if HMR becomes unreliable
-
Generate certificates.
Windows PowerShell:
.\docker\generate-certs.ps1WSL, Git Bash, or another POSIX shell:
./docker/generate-certs.sh
-
Create the Docker environment file.
Windows PowerShell:
Copy-Item .env.docker.example .env.dockerWSL, Git Bash, or another POSIX shell:
cp .env.docker.example .env.docker
-
Install host-side Node dependencies for frontend validation.
npm ci
This installs the local
node_modulesrequired by ESLint, TypeScript, Vitest, OpenAPI linting, and Playwright. -
Start Fast Mode.
npm run docker:dev:up
-
Trust
docker\traefik\certs\localhost.crton Windows if your browser warns about the local TLS certificate. -
Open the application.
- Main URL:
https://ernie.localhost:3333 - Localhost fallback after switching
ERNIE_DEV_HOSTandERNIE_DEV_SESSION_DOMAIN:https://localhost:3333
If
ernie.localhostdoes not resolve, add127.0.0.1 ernie.localhostto your hosts file. - Main URL:
-
Create the first administrator account.
npm run artisan -- add-user "Admin Name" admin@example.com SecurePassword
The Docker entrypoints install missing Composer dependencies and container-local npm dependencies, run migrations, and seed baseline data when the database is empty. Host-side frontend commands still require the local npm ci step above.
For day-to-day Laravel commands, use the npm wrappers. They run inside the app container, and generated files are written into the bind-mounted repository, so they still appear in your host checkout:
npm run artisan -- make:controller TestControllerDefault Fast Mode services:
- Traefik
- app
- webserver
- vite
- db
- redis
- queue
- scheduler
The scheduler waits for the application and database to become healthy, refreshes the persisted license usage ranking once at startup, and then runs Laravel's configured schedule continuously. The ranking is refreshed weekly after that initial backfill, so opening the Data Editor never needs to aggregate all stored resource-license associations.
Optional profiles:
assessmentstarts F-UJI plus the dedicatedassessment-queueworkers; setFUJI_ENABLED=truein.env.dockerwhen the app should use itparitystarts the same assessment services together with the parity profile; setFUJI_ENABLED=truein.env.dockerwhen the app should use it
Common startup commands:
npm run docker:dev:up
npm run docker:dev:assessment
npm run docker:dev:parityResource and IGSN assessments use database-backed runs and one short queue job per resource. Closing the browser, restarting a worker, or deploying the application therefore does not discard the run snapshot or its progress. Opening /assessment again reconnects to an active run; pressing Check while a run is active returns that same run, and a paused run is resumed instead of replaced.
The assessment profile starts two assessment-queue workers by default. Their shared Redis limiter permits at most 80 F-UJI requests per rolling 60-second window and spaces request starts by at least 750 ms. These defaults leave headroom below the local F-UJI limit of 100 requests per minute. To diagnose pressure, reduce FUJI_ASSESSMENT_CONCURRENCY to 1 before increasing any request limits.
The relevant settings are documented in .env.docker.example. Keep these relationships intact when changing them:
FUJI_ASSESSMENT_ITEM_TIMEOUTmust remain higher thanFUJI_TIMEOUT.FUJI_ASSESSMENT_LEASE_SECONDSandFUJI_ASSESSMENT_QUEUE_RETRY_AFTERmust remain higher than the item timeout.FUJI_ASSESSMENT_QUEUE_CONNECTIONmust use a persistent driver;syncandnullare rejected.- Every web and assessment-worker process must use the same Redis cache so request limiting and start locks are shared.
For a one-worker local load comparison, run:
docker compose --env-file .env.docker -f docker-compose.dev.yml --profile assessment up --build --scale assessment-queue=1| Task | Recommended place | Command |
|---|---|---|
| Start the core stack | Host shell | npm run docker:dev:up |
| Install host-side frontend dependencies | Host shell | npm ci |
| Start the backend services needed for PHP checks | Host shell | npm run docker:dev:backend:d |
| Stop the stack | Host shell | npm run docker:dev:down |
| Reset Docker volumes | Host shell | npm run docker:dev:reset |
| Laravel Artisan | npm wrapper into app container | npm run artisan -- <command> |
| Example controller generator | npm wrapper into app container | npm run artisan -- make:controller TestController |
| Refresh the license usage ranking | npm wrapper into app container | npm run artisan -- rights:update-usage-count |
| Composer | npm wrapper into app container | npm run composer:app -- <command> |
| Pest (2 GB, optimized complete suite) | Host shell via npm wrapper | npm run test:php |
| Pest deprecation details | Host shell via npm wrapper | npm run test:php:deprecations |
| MySQL-sensitive Pest slice | Host shell via npm wrapper | npm run test:php:mysql-sensitive |
| PHPStan | Host shell via npm wrapper | npm run phpstan:check |
| Vitest | Host shell | npm run test:run |
| Vitest performance diagnosis | Host shell | npm run test:doctor |
| ESLint check | Host shell | npm run lint:check |
| ESLint auto-fix | Host shell | npm run lint |
| TypeScript | Host shell | npm run types |
| TypeScript application watcher | Host shell | npm run types:watch |
| TypeScript test watcher | Host shell | npm run types:watch:test |
| Playwright against the dev stack | Host shell | npm run test:e2e:devstack |
| Canonical backend validation | Host shell | npm run check:backend |
| Canonical frontend validation | Host shell | npm run check:frontend |
| Canonical parity validation | Host shell | npm run check:parity |
.env.dockeris the Docker-oriented local environment file..envis the Laravel application environment used inside the containers.- The development entrypoint copies
.env.dockerto.envwhen.envdoes not already exist. - The npm Docker wrappers always pass
--env-file .env.dockerso Compose and Laravel use the same source of truth. - Docker-managed
node_moduleslive in the named Docker volume, not in your host checkout. - Complete Pest runs copy the current checkout into the Docker-managed
ernie-pest-workspacevolume before execution. This avoids repeated Windows/macOS bind-mount reads while leaving the development source mount and focused test workflow unchanged.
The local database service is pinned to MySQL 9.7, and its healthcheck verifies
the 9.7.x server series. MySQL-backed tests use this same service. Do not
downgrade the service to MySQL 8 when an existing data directory fails to start.
The Compose volume is named ernie-db-data-mysql-9-7 so an older
ernie-db-data volume cannot accidentally be mounted into MySQL 9.7. This is
necessary because MySQL 9.7 rejects a direct in-place upgrade from a data
directory created by a non-LTS MySQL 8.0 release. Compose creates and seeds the
new 9.7 volume on first startup; the old volume is retained and can be migrated
separately with a logical dump and restore if its local data is still needed.
Dockerfile and Dockerfile.dev also contain a MySQL 8.4 build stage. That
stage contributes only the separately named mysql-legacy-mysqldump client
needed to export the external MySQL 5.6 IGSN database. It never runs as ERNIE's
database server.
The administration log viewer can download the application log for the last
24 hours, 7 days, or 30 days. It reads both storage/logs/laravel.log and
Laravel's daily files named laravel-YYYY-MM-DD.log. A download can only
contain history that is still present on disk. Deployments using the daily
log channel must therefore keep LOG_DAILY_DAYS at 31 or more for complete
30-day downloads. The production template selects that channel with
LOG_STACK=daily; the development templates continue to use single. The
repository environment templates and Laravel's default use 31 days, while an
environment-specific override takes precedence.
The administrator-only /logs page includes CPU and memory history for the
entire host VM. Production and Stage enable the collector in their Compose
files. Only the scheduler container receives read-only bind mounts for the
host's /proc/stat and /proc/meminfo; the web application reads normalized
samples from MySQL and has no host filesystem access.
The scheduler records one sample per minute. The page displays five-minute averages for the last 24 hours or 30-minute averages for the last 7 days. Raw samples are retained for 30 days by default and pruned daily. The first sample after enabling monitoring or rebooting the VM establishes a CPU baseline, so a CPU percentage appears after the next consecutive sample. Missing intervals remain visible as gaps instead of being filled with zeroes.
Local collection defaults to SYSTEM_METRICS_ENABLED=false. Docker Desktop
would report the resource usage of its internal Linux VM rather than the
physical Windows or macOS computer, so /logs shows an intentional disabled
state locally. Parser and collector tests use deterministic fixtures and never
depend on the development host's /proc.
Useful Stage or Production checks are:
docker compose -f docker-compose.stage.yml exec scheduler test -r /host/proc/stat
docker compose -f docker-compose.stage.yml exec scheduler test -r /host/proc/meminfo
docker compose -f docker-compose.stage.yml exec scheduler php artisan system-metrics:collectNo Docker socket, privileged container, host PID namespace, or writable host
mount is required. Override SYSTEM_METRICS_RETENTION_DAYS only with a positive
number. Disabling collection preserves existing history until normal retention
removes it.
The administrator-only /logs page also shows the typical number of unique
signed-out visitors by weekday and hour for published landing pages and the DOI
and IGSN portals. The application deduplicates visitors for the current UTC
hour in the shared cache using a bucket-specific HMAC of IP address and user
agent. This HMAC identifier exists only in short-lived cache key names and
expires shortly after its UTC hour. Only hourly counters are persisted; no IP
address, user agent, cookie, session identifier, or visitor hash is stored in
MySQL or application logs. Authenticated users, empty user agents, and
recognizable crawlers are excluded. Extend the built-in
heuristic list with a comma-separated
BOT_PROTECTION_ADDITIONAL_CRAWLER_USER_AGENTS value when required.
PUBLIC_TRAFFIC_ENABLED defaults to false locally. Stage and Production
enable it in Compose. Their scheduler runs
public-traffic:observe-availability every minute against the configured
public /health URL and verifies the shared cache before marking the minute as
observed. Only completed hours with all 60 minutes observed contribute to the
heatmap. This keeps a healthy zero-visitor hour distinct from an application or
analytics outage. Aggregates are retained for 400 days by default and pruned
daily.
For a controlled local check, enable the feature and point the probe at a trusted reachable health URL before running:
docker compose --env-file .env.docker -f docker-compose.dev.yml exec app php artisan public-traffic:observe-availability
docker compose --env-file .env.docker -f docker-compose.dev.yml exec app php artisan public-traffic:pruneThe UI groups UTC buckets in Europe/Berlin, including daylight-saving time,
and offers rolling 4-, 12-, and 52-week views. Recommendations remain in the
collecting state until every weekday/hour cell has at least one complete
observation.
Keep DATACITE_TEST_MODE=true for local development and Stage. Eligible imported resources and every newly imported IGSN receive their local landing page, but the import never writes metadata to either DataCite API in this mode.
GEOFON Seismic Events keep the external landing-page target supplied by DataCite when it points to a GEOFON host. Legacy http:// GEOFON URLs are stored as https://, and retired /db/eqpage.php?id=... event targets are stored directly under the canonical https://geofon.gfz.de/eqinfo/event.php?id=... URL. Findable records receive a published external landing page, so their ERNIE workflow status is published; this applies based on the selected or assigned datacenter and is not limited to one DOI prefix.
With DATACITE_TEST_MODE=false on Production, the same local landing pages are created and the newly imported, published records enter a separate DataCite synchronization phase. That phase exports the complete ERNIE metadata and changes the DOI target URL to the new landing page. Failed updates do not roll back the import or landing page and can be retried from the completed import dialog.
Production uses separate DataCite Repository accounts for ordinary GFZ DOIs and legacy IGSNs. Configure DATACITE_USERNAME / DATACITE_PASSWORD and DATACITE_CLIENT_ID for the ordinary DOI repository and DATACITE_IGSN_USERNAME / DATACITE_IGSN_PASSWORD for the GFZ.IGSN repository that owns prefix 10.60510. ERNIE selects the IGSN credentials automatically for that prefix; using the ordinary DOI credentials results in a DataCite HTTP 403 response. Audits against the test API additionally require DATACITE_TEST_CLIENT_ID.
Use the authenticated, dry-run-first repair command for existing GEOFON Seismic Events Resources whose external landing page still uses the retired db/eqpage.php route. It checks the local and DataCite URLs independently, requires the event ID in both URLs to match the DOI suffix, and accepts only the known GEOFON event DOI namespaces and hosts. It also reports a matching retired local URL assigned to any other datacenter as manual review and never updates that Resource.
Start with a complete audit:
npm run artisan -- resources:repair-geofon-event-landing-page-urls \
--report=storage/app/geofon-event-url-audit.csvThe dry run performs authenticated DataCite GETs and bounded reachability checks against the canonical https://geofon.gfz.de/eqinfo/event.php?id=... target, but it sends no PUT and changes no local row. Review every wrong-datacenter, unknown URL, event-ID mismatch, unreachable target, and API error before applying anything.
Apply and verify one representative DOI first:
npm run artisan -- resources:repair-geofon-event-landing-page-urls \
--apply --force-production \
--doi=10.1594/gfz.geofon.gfz2011axdw \
--report=storage/app/geofon-event-url-pilot.csvFor each required DataCite update, ERNIE saves the complete source record below storage/app/private/geofon-event-url-updates/, sends only the new url, and confirms it with another authenticated GET. The local domain and path are updated transactionally only after DataCite is confirmed; an independently stale local URL is still repairable when DataCite is already current. Authentication failures stop subsequent records, while per-record validation, reachability, verification, and concurrency failures remain visible in the CSV without overwriting local data.
After the pilot resolves directly to the canonical GEOFON page, run the complete apply and then repeat the dry run:
npm run artisan -- resources:repair-geofon-event-landing-page-urls \
--apply --force-production \
--report=storage/app/geofon-event-url-applied.csv
npm run artisan -- resources:repair-geofon-event-landing-page-urls \
--report=storage/app/geofon-event-url-verification.csvThe final dry run must report every eligible Resource as already_current. Use repeatable --doi filters for targeted retries, --after-id=<resource-id> to resume after the last processed Resource, and --limit=<count> for bounded batches. Production writes always require both --apply and --force-production; test mode never reaches the production API.
Every metadata update for an existing DOI now asks DataCite to store the record with Kernel 4. In the Data Editor, any non-empty DOI therefore produces the Update Metadata action regardless of the local draft, curation, review, or published status. This is an update of the existing DOI, not another registration.
Use the authenticated, dry-run-first upgrade command to normalize already imported repository records. It lists every state visible to the configured Repository account, intersects the result with local ERNIE resources by normalized DOI, and excludes IGSNs and unconfigured prefixes. No database migration is involved.
Start with an audit and inspect all manual_review, not_imported, and error rows:
npm run artisan -- resources:upgrade-datacite-schema \
--report=storage/app/datacite-schema-upgrade-dry-run.csvThe dry run never sends a PUT. Kernel 3, Kernel 2.x, and records without a schema version are automatically eligible only when types.resourceTypeGeneral is valid for Kernel 4 and no legacy contributorType: Funder remains. Unknown schema versions and incompatible metadata stay unchanged for manual review.
Apply one representative DOI from each legacy group first:
npm run artisan -- resources:upgrade-datacite-schema \
--apply --force-production \
--doi=10.5880/example \
--report=storage/app/datacite-schema-upgrade-pilot.csv--force-production is mandatory only when --apply targets the production API. It is an explicit safety acknowledgement; the command remains non-interactive and still performs every per-record preflight. Confirm that the pilot retained its DOI state, landing-page URL, and resource type before running the complete upgrade:
npm run artisan -- resources:upgrade-datacite-schema \
--apply --force-production \
--report=storage/app/datacite-schema-upgrade-applied.csvApply runs send only doi, schemaVersion, and the existing types object. Before each PUT, the complete DataCite source record is saved below storage/app/private/datacite-schema-upgrades/; this location is not publicly served. API or verification failures are isolated in the CSV, except that HTTP 401 or 403 stops subsequent writes. Resume a bounded run with --after-id=<resource-id> and --limit=<count>, or retry individual records with repeatable --doi options.
After applying, repeat the dry run. Successfully upgraded records appear as already_current and do not cause another write, making the process idempotent. Archive the apply report and private snapshots according to the operating retention policy.
Newly created resources from DataCite are optionally enriched with profile lines from non-empty sumario-pmd.coverage.wkt values. ERNIE stores each valid coordinate chain as geo_type = line with its ordered points in polygon_points, so the Data Editor and landing pages retain the original line geometry. On later DataCite exports, the existing thin-polygon workaround is used because DataCite does not provide a line geometry type.
The enrichment replaces a DataCite bounding box only when all four legacy bounds match and exactly one safe candidate can be identified, using the place description to resolve equal boxes where possible. Other GeoLocations are preserved. Invalid geometry, incomplete bounds, ambiguous matches, and legacy database failures keep the imported DataCite metadata and do not fail the import.
This enrichment runs only while creating a new DataCite resource. Duplicate, skipped, and repair paths do not add lines to resources that already exist in ERNIE; there is no automatic backfill.
New SUMARIO imports correct the duplicated paragraph breaks produced by the legacy XML export before storing descriptions. The correction is pairwise: two consecutive <br> tags become one, three become two, four become two, and in general a run of n break tags or plain-text newline tokens becomes ceil(n / 2). Whitespace between tags and the variants <br>, <br/>, and <br /> are supported; unrelated text and HTML remain unchanged.
The migration adds resources.legacy_description_breaks_normalized_at as a durable one-time marker because applying the pairwise rule twice would remove intentional spacing. The cleanup considers both resources marked with legacy_source = sumario-pmd and older unmarked resources whose normalized DOI has exactly one match in SUMARIO. Ambiguous DOI matches are reported for manual review. The configured metaworks connection must therefore be reachable for every run, although the command never writes to the legacy database.
Deploy the migration, take the normal ERNIE database backup, and audit the complete selection before applying changes:
npm run artisan -- migrate --force
npm run artisan -- resources:repair-legacy-description-breaks \
--report=storage/app/legacy-description-breaks-dry-run.csv
npm run artisan -- resources:repair-legacy-description-breaks \
--apply --after-id=0 --limit=500 --chunk=100 \
--report=storage/app/legacy-description-breaks-applied.csvUse repeatable --doi or --legacy-id options for targeted audits. --after-id refers to the ERNIE resources.id; the command always prints the last scanned ID, including batches containing only non-legacy candidates whose CSV has no data rows. Review the CSV and use that printed ID before continuing with another bounded batch. Apply runs update all descriptions of one resource transactionally, reject concurrent edits, invalidate a changed published landing-page cache, and never process an already marked resource again.
With DATACITE_TEST_MODE=false, every changed resource with a DOI is queued for a complete metadata synchronization through the imports queue. In test mode the local repair still applies but no DataCite request is made. Sync failures do not roll back local changes; retry them with the run UUID printed by the apply command:
npm run artisan -- resources:repair-legacy-description-breaks --retry-sync=<sync-run-uuid>The temporal-coverage migration adds nullable columns to geo_locations; it does not guess or manufacture values for existing rows. After deploying the migration, the original sumario-pmd.coverage.start and coverage.end values can be copied into already imported ERNIE resources with the dry-run-first backfill command.
By default, the command considers only resources with the exact legacy_source = sumario-pmd and legacy_source_id recorded by the SUMARIO import. It matches each legacy coverage to an existing GeoLocation by its spatial coordinates, then uses a normalized description or the original one-to-one position only where that is unambiguous. A legacy coverage without spatial identity is added as a temporal/place-only GeoLocation. Existing equal values are left unchanged. If any temporal field conflicts, the matched GeoLocation remains completely unchanged and is reported for manual review; missing fields are filled only when the complete merge is conflict-free.
Run the migration and audit before applying changes:
npm run artisan -- migrate --force
npm run artisan -- resources:backfill-legacy-temporal-coverages --report=storage/app/legacy-temporal-coverage-dry-run.csvReview every manual_review, missing_legacy, and error row in the CSV. Then apply the safe rows in bounded batches:
npm run artisan -- resources:backfill-legacy-temporal-coverages --apply --after-id=0 --limit=500 --chunk=100 --report=storage/app/legacy-temporal-coverage-applied.csvUse repeatable --doi or --legacy-id options for a targeted rollout. --after-id always refers to the ERNIE resources.id shown in the report. The command is idempotent and invalidates the rendered cache of a changed published landing page.
Imports created before ERNIE recorded legacy_source_id require an explicit DOI fallback. Audit a small selection first because this mode also inspects otherwise unlinked ERNIE resources whose DOI exists in SUMARIO:
npm run artisan -- resources:backfill-legacy-temporal-coverages --match-by-doi --doi=10.5880/example --report=storage/app/legacy-temporal-coverage-doi-audit.csv
npm run artisan -- resources:backfill-legacy-temporal-coverages --apply --match-by-doi --doi=10.5880/exampleThe application and queue containers need working access to the configured metaworks connection while the command runs. The backfill reads but never modifies the legacy database. Take the normal ERNIE database backup before the apply run; a nonzero exit code indicates processing errors, while manual-review rows deliberately remain unchanged and do not fail the complete run.
Metadata imports classify each DataCite subject only once. Resources imported before that fix can be audited with a dry-run-first command. By default it considers controlled subjects only and treats rows as duplicates only when value, language, scheme, scheme URI, value URI, classification code, and breadcrumb path are all exactly equal. The smallest Subject ID survives.
Audit GEMET and MSL first and retain the CSV for review:
npm run artisan -- subjects:deduplicate \
--scheme="GEMET - GEneral Multilingual Environmental Thesaurus" \
--scheme="EPOS MSL vocabulary" \
--report=storage/app/subject-duplicates-dry-run.csvAfter taking the normal database backup and reviewing the report, apply the same bounded selection:
npm run artisan -- subjects:deduplicate --apply \
--scheme="GEMET - GEneral Multilingual Environmental Thesaurus" \
--scheme="EPOS MSL vocabulary" \
--after-resource-id=0 --limit=500 --chunk=100 \
--report=storage/app/subject-duplicates-applied.csvThe repeatable --doi option narrows an audit further. Use --include-free only when exact free-keyword duplicates should also be considered. Apply runs are transactional per resource and idempotent; they remove stale Subject assistance rows and invalidate affected keyword and landing-page caches. A nonzero exit code indicates processing errors.
Keep DATACITE_TEST_MODE=true and use test Repository credentials when exercising the Data Editor's Register or Update Metadata actions locally. Validate, Save Draft, autosave, Preview LP, and Show LP are local-only actions and must not produce a DataCite request. The two DataCite write actions run complete client validation, show an explicit confirmation before their action-specific save, and automatically continue through landing-page setup when a page is missing.
After a new test DOI is registered, the success dialog displays the number of locally published Resources plus published IGSNs. This is deliberately smaller than or equal to the combined sidebar badges: a record counts only when it has a non-empty DOI and a published landing page. Draft, curation, and review records are excluded. The dialog redirects to /resources after five seconds; operating-system reduced-motion settings suppress confetti without changing the result or countdown.
There is no separate post-import sync flag: DATACITE_TEST_MODE is the only switch. The queue worker must consume the imports queue so the bounded synchronization jobs can complete.
The IGSN list can queue up to 1000 selected samples for DataCite registration or metadata updates. Keep DATACITE_TEST_MODE=true, configure valid DataCite Test credentials, and use only disposable identifiers when testing this workflow locally. The start request returns 202 Accepted; it never performs the DataCite writes in the web request.
Each run and its ordered items are stored in igsn_registration_runs and igsn_registration_items. One short-lived job processes one item and dispatches the next job on the queue configured by DATACITE_QUEUE, which defaults to datacite. Both the app and queue services must use a persistent queue connection such as database; sync and null are deliberately rejected. Every Docker environment forwards DATACITE_QUEUE to both services, and its worker consumes the configured queue.
The effective DataCite test/production mode and endpoint are snapshotted when a run starts. Credentials are never stored with the run and are read from the current server configuration by each job. A changed mode or endpoint pauses the run before another write. Beginner runs remain on DataCite Test even when a worker executes without a signed-in browser user.
Closing the progress dialog, navigating away, restarting a worker, or reloading the browser does not cancel a run. Return to /igsns and use View registration progress to inspect it. Cancellation takes effect between external requests. Failed items can be retried without resending successful items; a resumed item also checks whether DataCite already accepted an earlier create request before it attempts another create.
For a safe local check:
- Start the Docker stack and confirm the queue service consumes
datacite. - Create a small set of synthetic IGSNs with published landing pages.
- Confirm the progress dialog says DataCite Test, then use Register Selected.
- Close and reopen the dialog while the run is active, and verify the same run and counters return.
- Exercise cancellation or retry only with disposable test identifiers. Never use production credentials for automated or local development tests.
Single-IGSN imports preload their complete DataCite family and the corresponding legacy DIF metadata before writing resources. The public legacy IGSN portal is the mandatory source for this strict preflight. If the portal is unreachable, returns invalid JSON, or contains malformed DIF data, the complete single import fails without creating any new resource, IGSN metadata, relationship, datacenter assignment, or landing page. Existing ERNIE resources remain unchanged and are not backfilled.
The default portal endpoint and retry settings are defined by:
GFZ_IGSN_PORTAL_PROXY_URLGFZ_IGSN_PORTAL_CONNECT_TIMEOUTGFZ_IGSN_PORTAL_TIMEOUTGFZ_IGSN_PORTAL_RETRY_TIMESGFZ_IGSN_PORTAL_RETRY_SLEEP_MSGFZ_IGSN_PORTAL_RETRY_JITTER_MS
Keep the portal URL on HTTPS. Retries include the observed failure mode in which the proxy answers with HTTP 200 but the body is not valid JSON. The queue job records a stable error_code such as legacy_source_unavailable or legacy_invalid_payload in the import progress instead of silently creating a DataCite-only partial record.
Authenticated Solr and the direct legacy database remain optional enrichment sources for non-single imports. Enable IGSN_LEGACY_DB_ENABLED only after the configured TLS and credentials have been verified from both the app and queue containers. Network reachability of port 3306 alone does not prove that the legacy database connection works.
Both app and queue services must use QUEUE_CONNECTION=database, and the worker command must consume imports. The single-import start endpoint returns 202 Accepted; later portal or persistence failures are reported through the import status endpoint.
New legacy imports inspect every non-empty DIF 1.1, 1.2, and 1.3 leaf across all sample blocks and project the approved metadata into typed ERNIE columns and relations. This includes root hasDocument publication DOIs as DataCite Cites, funders, Available and Collected dates, contributors, geological ages, Rock Type, request information, structured methods, drilling lengths, launch/navigation values, and elevation ranges. ERNIE does not retain a raw DIF payload: unknown non-empty paths are reported with their complete namespace-independent path and sample index so that they require an explicit mapping decision. The known supplemental IsCitedBy copies are intentionally ignored because their direction is wrong and the equivalent root publications are authoritative. Total Length remains local IGSN drilling metadata and is never exported as DataCite sizes.
Use the dry-run-first command to audit and repair IGSNs already stored in ERNIE across all configured legacy datacenters:
npm run artisan -- igsn:backfill-legacy-dif-metadata --report=storage/app/igsn-dif-dry-run.csv
npm run artisan -- igsn:backfill-legacy-dif-metadata --datacenter=IGSNDB.ICDP --doi=ICDP5052ECZI101
npm run artisan -- igsn:backfill-legacy-dif-metadata --apply --report=storage/app/igsn-dif-applied.csvReview missing DIF records, unknown non-empty paths, scalar and privacy conflicts, image availability, and technical errors before using --apply. Apply mode is additive: it fills missing values and appends missing repeated values while preserving conflicting local values for manual review. The same command validates allowlisted external ICDP sample images. A definitive 404/410 or invalid image response is reported as unavailable and removes only the public sample_image_external_url; the validated source descriptor remains stored for auditing. A timeout, transport failure, rate limit, or temporary 5xx response is reported as an image probe error and does not remove a previously published URL. Repeat --doi or --datacenter for a restricted scope, use --limit for a bounded batch, and resume after the printed Resource ID with --after-id. Portal requests are limited to 100 handles per request even when a larger --chunk is supplied.
The console summary reports unavailable images and temporary probe errors separately. CSV reports include sample_image_status, sample_image_url, and sample_image_message. Image-only changes use the same transaction, landing-page cache invalidation, DataCite synchronization, CSV audit, and --retry-sync path as every other field handled by igsn:backfill-legacy-dif-metadata; no additional repair command is required for legacy DIF metadata.
Every changed registered IGSN is queued automatically for a full-metadata DataCite synchronization after its local transaction completes. The console and CSV report contain the sync-run UUID. Local changes remain intact if an asynchronous DataCite item fails; retry only those failures with:
npm run artisan -- igsn:backfill-legacy-dif-metadata --retry-sync=<sync-run-uuid>After each applied scope, repeat the same dry run. It should report no further automatic changes; unresolved conflicts, unknown paths, and missing DIF records remain visible for manual follow-up.
New legacy imports preserve all supported classifications from every <sample> block in their original order. This includes the Medusa, Sonne273, Earth Shape, and ICDP values covered by issues #1191, #1200, #1202, and #1210. Unknown controlled values remain rejected and are reported without rolling back unrelated DIF metadata.
Existing IGSNs are deliberately skipped by the DataCite importer, but they no longer need to be deleted and reimported to recover classifications. The dedicated command audits every imported IGSN, fetches its DIF metadata from the public legacy portal in batches of at most 100, and only appends missing classifications or fills an empty classification type. Existing values, positions, and non-empty types are never removed or overwritten. The command is a dry run unless --apply is supplied:
npm run artisan -- igsn:backfill-classifications --doi=ICDP5054ES1O201
npm run artisan -- igsn:backfill-classifications --report=storage/app/igsn-classification-dry-run.csv
npm run artisan -- igsn:backfill-classifications --apply --report=storage/app/igsn-classification-applied.csvDeploy the updated classification catalogs and application code before running the command. Review rejected values, type conflicts, missing DIF documents, and technical errors in the dry-run report before applying changes globally. Use --limit for a bounded run, repeat --doi for selected handles or DOIs, and resume after the last completed Resource ID with --after-id. A successful apply invalidates only changed published landing-page caches. A second global dry run must report no remaining supported classifications as would_update.
The command also repairs still-incomplete classification data from the earlier issues, so their former delete-and-reimport procedure is obsolete. The separate igsn_metadata.user_code schema correction from issue #1192 still requires its existing migration. The rejected vocabulary request from issue #1201 remains intentionally excluded.
When a legacy DIF record contains a sample image, the import stores its validated source description with the IGSN metadata. Known GFZ Data Services images are downloaded only after the metadata transaction has committed and are served from the persistent Laravel public disk. Known ICDP image URLs are normalized to https://data.icdp-online.org/..., checked with a bounded allowlist-protected request, and published as external images only after a valid JPEG response. Definitive missing responses are reported as unavailable and do not produce a public image card; temporary probe failures remain distinct and retryable. Unknown hosts, unsafe paths, placeholders, invalid MIME types, oversized files, and failed downloads likewise do not roll back an otherwise successful metadata import. The completed import dialog reports unavailable images separately from technical failures.
Configure the storage disk and download limits with IGSN_IMAGE_DISK, IGSN_IMAGE_CONNECT_TIMEOUT, IGSN_IMAGE_TIMEOUT, and IGSN_IMAGE_MAX_BYTES. External probes additionally use IGSN_IMAGE_EXTERNAL_PROBE_TIMEOUT and IGSN_IMAGE_EXTERNAL_PROBE_MAX_BYTES, which default to 10 seconds and 256 KiB. The managed-image size limit defaults to 20 MiB and only validated JPEG files are accepted. In production, the selected disk must be persistent, backed up, and publicly linked in the same way as the existing Laravel public disk.
The older image-only backfill deliberately considers only IGSNs that already exist in ERNIE. It remains available for downloading or replacing managed GFZ images, but it is not required to repair the complete Legacy-DIF projection or remove unavailable external ICDP links. It is a dry run unless --apply is supplied:
npm run artisan -- igsn:backfill-images --doi=GFSO273N39
npm run artisan -- igsn:backfill-images --apply --after-id=0 --chunk=100 --report=storage/app/igsn-image-backfill.csvUse --limit for bounded rollout batches, repeat --doi to select multiple handles or DOIs, and use --force only when already processed images must be revalidated or replaced. A failed run can resume after the last reported resource ID with --after-id. The command is idempotent; missing legacy DIF records and records without an image are reported separately from real processing failures.
IGSN landing-page templates expose every IGSN module, including Sample Image and Location, in a shared two-column editor. Modules can be reordered within a column or moved across columns; each module must occur exactly once across the saved layout. The built-in Templates IGSN copy template places Sample Image in the right column immediately before Location.
Cloned Resource landing-page templates use the same two-column interaction, but expose only Resource modules. Every module can be reordered or moved between columns and must occur exactly once across the complete layout. Description, people, funding, keyword, and metadata-download modules retain the existing shared metadata card in each occupied column; moving them does not create separate cards. The built-in Templates Resources copy template remains immutable and keeps the canonical layout used for new clones.
The admin-only actions on /resources and /igsns use a persistent queue and the shared application cache. The Docker worker consumes the dedicated datacite queue. Queue connections whose configured driver is sync or null are rejected regardless of the connection name, because the run must survive request timeouts, browser navigation, deployments, and worker restarts.
Keep the safe rate defaults from .env.example: at most 300 authenticated requests per rolling five-minute window, at least one second between requests, concurrency one, a 10-second connection timeout, and a 30-second request timeout. Target landing-page reachability checks have separate configurable connection and total timeouts of three and eight seconds. All DataCite writes in ERNIE share this limiter. Redis or another cache shared by every web and queue process is therefore required; an in-process array cache is not safe outside automated tests.
After a domain move, the configured HTTPS APP_URL is the sole source for every new landing-page URL. There is deliberately no separate old-host or expected-new-host setting. DATACITE_USER_AGENT_EMAIL should identify the operational contact DataCite can reach.
For a local dry run, use DataCite test credentials and DATACITE_TEST_MODE=true, configure APP_URL to the HTTPS base URL being tested, and make sure the generated target URLs are actually reachable from the app/queue containers. Never place production credentials in a local environment.
- A plain URL switch to
https://localhost:3333is not enough whileERNIE_DEV_SESSION_DOMAIN=ernie.localhost. - For a localhost fallback, set
ERNIE_DEV_HOST=localhostandERNIE_DEV_SESSION_DOMAIN=localhostin.env.docker, keeplocalhost:3333inERNIE_DEV_STATEFUL_DOMAINS, then restart the stack.
Docker Desktop can fail to sync the file back to the host even when it exists in the container.
docker compose --env-file .env.docker -f docker-compose.dev.yml exec vite sh -c 'echo "https://ernie.localhost:3333" > /var/www/html/public/hot'That is expected unless the matching profile was started:
npm run docker:dev:assessmentnpm run docker:dev:parity
For F-UJI specifically, the app still treats the integration as disabled until FUJI_ENABLED=true is set in .env.docker and the stack is restarted.
The initial boot may still need to:
- build images
- install Composer dependencies
- install npm dependencies
- run migrations
- seed baseline data
- calculate the initial license usage ranking
Subsequent startups are usually much faster because Docker volumes keep vendor, node_modules, and the MySQL data directory.
The Data Editor orders active licenses by the persisted rights.usage_count value and uses the license name as the tie-breaker. If every counter is 0, the whole list therefore appears alphabetical.
Check that the scheduler service is running and inspect its startup output. For an immediate diagnostic refresh, run:
npm run artisan -- rights:update-usage-countRestarting the scheduler also performs this refresh before schedule:work starts. A failed startup refresh leaves the previous complete counter snapshot in place and causes the scheduler container to restart instead of silently serving a partially updated ranking.
The default local Pest loop remains SQLite-backed.
Use the dedicated MySQL-backed slice only when driver-sensitive verification is required:
npm run test:php:mysql-sensitiveThis command:
- starts the backend containers if needed
- verifies and uses the MySQL 9.7 service from
docker-compose.dev.yml - creates an isolated
ernie_testschema inside that local MySQL container - runs the current explicit MySQL-sensitive migration file slice with a schema reset before each file
It does not reuse the regular development schema. For broader testing guidance, see testing.md.