Skip to content

Share-readiness check: warn when a shared project references data recipients cannot load - #1812

Merged
giswqs merged 5 commits into
mainfrom
feat/issue-1671-share-readiness-check
Aug 10, 2026
Merged

Share-readiness check: warn when a shared project references data recipients cannot load#1812
giswqs merged 5 commits into
mainfrom
feat/issue-1671-share-readiness-check

Conversation

@giswqs

@giswqs giswqs commented Aug 10, 2026

Copy link
Copy Markdown
Member

Fixes #1671.

A .geolibre.json is mostly references. The publish path embeds local vector data, but every tile template, COG, PMTiles endpoint, OGC service, and hosted feature service stays a URL. So a project can upload cleanly and still draw nothing for the recipient, and the author finds out when someone tells them the map is empty (if they tell them).

What it does

When the Share dialog opens, it classifies every data source the project references, probes the ones that need the network, and lists the ones a recipient could not load with a reason and a fix. It informs, it does not block the Share button.

Verdicts covered:

Reason What the author is told
Credential stripped on upload (URL param, populated credential field, Google photorealistic 3D Tiles) Make the service public, or have recipients supply their own key
401/403 to an anonymous request Same
Browser fetch rejected (no CORS headers, TLS, unreachable) Still loads in the desktop app, not in the browser viewer
404/410 The link may have expired or moved
Filesystem path, blob:, private-network or single-label host, a query-backed layer with no source at all Convert to a hosted source, or expect an empty layer

Implementation

apps/geolibre-desktop/src/lib/share-readiness.ts, split so the classification is pure and unit-testable:

  • collectShareSources walks every place a layer can hide a reference (source.url/data/tiles/urls/baseUrl/arcgisQueryUrl, metadata.originalUrl/tileUrl/localFilePath/localBytesUrl, sourcePath) plus the basemap style URL and plugin manifest URLs, de-duplicates the template an XYZ layer repeats across three fields, and settles everything that needs no network.
  • probeShareSources resolves the rest: one anonymous HEAD per distinct target, with a one-byte ranged GET retry for the object stores and CDNs that refuse HEAD, credentials: "omit", a 6s per-request timeout and a 16-target budget. A 5xx is reported as unchecked rather than blamed on the project.

Two things it reuses rather than re-derives, so it cannot drift from the publish path:

  • which layers get embedded comes from isEmbeddableLocalVectorLayer, the same predicate buildEmbeddedLayers uses;
  • whether a credential survives the upload comes from redactUrlCredentials / isCredentialFieldName, the same rules the upload applies.

Two deliberate choices worth calling out:

  • Tile templates collapse to their origin before probing. Substituting a nominal 0/0/0 tile 404s on any service whose data starts deeper, which would report a healthy basemap as missing. The origin still answers what the check asks: is the host up, does it send cross-origin headers, does it want a credential.
  • The probes go through the browser's fetch, not the desktop app's native HTTP bypass, so a cross-origin rejection surfaces as one instead of being masked by a request that is not subject to CORS. That is what the recipient's browser will do.

Testing

  • tests/share-readiness.test.ts: 23 new cases over classification, probing (HEAD/ranged-GET, status mapping, opaque rejection, 5xx, budget cap), and aggregation, with an injected fetchImpl.
  • Full frontend suite: 5622 passing, 0 failing.
  • Verified in the browser with Playwright in both light and dark themes, against a project carrying a public XYZ layer, a keyed MapTiler template, a local dem.tif, an intranet WMS, a moved COG, and an inline GeoJSON layer. Four rows reported with the right verdicts; the public layer, the basemap, and the inline layer correctly stayed out of the list. A clean project shows the "All N data sources look reachable" line instead. No console errors.
  • All 20 new strings translated into all 18 locale catalogs.
  • Docs: a "Share-readiness check" section in docs/user-guide/projects.md and a bullet in docs/features.md.

Summary by CodeRabbit

New Features

  • Added a pre-sharing readiness check for project layers, basemaps, plugins, and local vector sources.
  • Shows accessibility counts, source details, warnings, causes, truncation notices, and recommended fixes.
  • Checks run without user credentials and do not block sharing when issues are detected.

Localization

  • Added readiness messages across supported languages.

Documentation

  • Documented readiness checks and common data-source issues.

…nnot load

A `.geolibre.json` is mostly references. The publish path embeds local
vector data, but every tile template, COG, PMTiles endpoint, OGC service,
and hosted feature service stays a URL. So a project can upload cleanly
and still draw nothing for the recipient: the service wanted a token the
author holds and the upload strips, the host sends no cross-origin
headers so it works in the desktop app and fails in the browser viewer,
or the reference never left the author's machine. The author finds out
when someone tells them the map is empty, if they tell them.

Add a pre-flight readiness check that runs when the Share dialog opens.

`share-readiness.ts` splits it in two so the classification stays pure
and testable. `collectShareSources` walks every place a layer can hide a
reference (`source.url`/`data`/`tiles`/`urls`/`baseUrl`/`arcgisQueryUrl`,
`metadata.originalUrl`/`tileUrl`/`localFilePath`/`localBytesUrl`,
`sourcePath`) plus the basemap style and plugin manifests, and settles
what needs no network: a filesystem path, a private-network or
single-label host, a URL whose credential `redactUrlCredentials` will
strip, a populated credential field, a query-backed layer that names no
source at all. `probeShareSources` resolves the rest with one anonymous
HEAD per distinct target (a ranged GET retry for hosts that refuse HEAD),
capped and short-timed out.

Two things the classification reuses rather than re-derives: which layers
the publish path embeds comes from `isEmbeddableLocalVectorLayer`, and
whether a credential survives the upload comes from the same redaction
rules the upload applies. A tile template collapses to its origin before
probing, because substituting a nominal 0/0/0 tile 404s on any service
whose data starts deeper and would report a healthy basemap as missing.

The probes deliberately go through the browser's `fetch`, not the desktop
app's native HTTP bypass, so a cross-origin rejection surfaces as one
instead of being masked by a request that is not subject to CORS. That is
what the recipient's browser will do.

The dialog lists one row per layer with its worst verdict, a
plain-language reason, and a fix. It never gates the Share button: an
author sharing an intranet map with intranet colleagues is doing the
right thing and should not have to fight a warning.

Fixes #1671
Copilot AI lite review requested due to automatic review settings August 10, 2026 00:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 37fd8131-3214-4761-a46c-39391c121100

📥 Commits

Reviewing files that changed from the base of the PR and between a4b2bcb and f172cfc.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/lib/share-readiness.ts
  • packages/core/src/credentials.ts
  • packages/core/src/index.ts
  • tests/share-readiness.test.ts

📝 Walkthrough

Walkthrough

The PR adds client-side share-readiness checks. It classifies project sources, probes eligible URLs anonymously, reports recipient accessibility issues, and displays localized guidance in the share dialog without blocking uploads.

Changes

Share-readiness validation

Layer / File(s) Summary
Source models and collection
apps/geolibre-desktop/src/lib/share-readiness.ts, packages/core/src/credentials.ts, packages/core/src/index.ts, tests/share-readiness.test.ts
The new module defines readiness types, classifies source references, detects credentials and private hosts, collects project sources, and excludes embedded data. The package exports the credential helpers needed by the check. Tests cover collection and classification.
Anonymous probing and readiness aggregation
apps/geolibre-desktop/src/lib/share-readiness.ts, tests/share-readiness.test.ts
Eligible HTTP(S) sources use anonymous HEAD or ranged GET requests with timeouts, fallback handling, deduplication, probe limits, status aggregation, and ordered problem reporting. Tests cover probing, aggregation, truncation, and missing-fetch fallback.
Share dialog feedback and localization
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx, apps/geolibre-desktop/src/i18n/locales/*.json, docs/features.md, docs/user-guide/projects.md
The dialog runs an abortable advisory check on open and displays progress, source problems, advice, truncation details, or reachable-source counts. Locales and documentation describe the new states and reasons.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ShareProjectDialog
  participant checkShareReadiness
  participant BrowserFetch
  ShareProjectDialog->>checkShareReadiness: snapshot project sources
  checkShareReadiness->>BrowserFetch: anonymous HEAD or ranged GET
  BrowserFetch-->>checkShareReadiness: status or blocked/network error
  checkShareReadiness-->>ShareProjectDialog: readiness report
  ShareProjectDialog-->>ShareProjectDialog: render localized warnings
Loading

Possibly related PRs

Poem

I test each path before the share,
With tiny probes sent through the air.
The rabbit reads each warning bright,
Then helps each source become reachable right. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: warning when shared project data sources are inaccessible to recipients.
Linked Issues check ✅ Passed The implementation covers source inspection, anonymous probing, failure classification, localized per-source guidance, non-blocking UI behavior, tests, and documentation for issue #1671.
Out of Scope Changes check ✅ Passed The changes remain within scope for issue #1671, including implementation support, tests, translations, documentation, and credential-redaction reuse.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-1671-share-readiness-check

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://132d9201.geolibre-preview.pages.dev
Demo app https://132d9201.geolibre-preview.pages.dev/demo/
Commit f172cfc

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx`:
- Line 2: Update the import of isEmbeddableLocalVectorLayer in
ShareProjectDialog.tsx to use its direct defining module in vector-layer-sync.ts
instead of the `@geolibre/plugins` barrel, without changing how the symbol is
used.

In `@apps/geolibre-desktop/src/lib/share-readiness.ts`:
- Around line 473-493: Update probeTarget’s request flow to create a single
timeout/deadline signal before the HEAD attempt, then reuse that signal when
composing the optional caller signal for both HEAD and ranged GET requests.
Remove the per-call AbortSignal.timeout creation inside request, while
preserving the existing retry and response-status behavior.

In `@tests/share-readiness.test.ts`:
- Around line 295-312: Update the test around probeShareSources to record each
request’s method and headers, then assert that the first attempt is HEAD and the
retry is GET with Range set to bytes=0-0. Keep the existing reachable-status
assertion so the test covers both the request contract and final outcome.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 47502513-0de8-47e9-b17c-0b7adaea633f

📥 Commits

Reviewing files that changed from the base of the PR and between 337e06c and 389508c.

📒 Files selected for processing (24)
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/share-readiness.ts
  • docs/features.md
  • docs/user-guide/projects.md
  • packages/core/src/index.ts
  • tests/share-readiness.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts Outdated
Comment thread tests/share-readiness.test.ts
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found with high confidence. The core classification/probing logic (share-readiness.ts) is internally consistent, correctly reuses isEmbeddableLocalVectorLayer, redactUrlCredentials, and isCredentialFieldName so it can't drift from the actual publish/redaction path, and its edge cases (private hostnames, tile-template collapsing, HEAD-refusing hosts, 5xx handling, abort/timeout classification, probe budget) all match their test coverage. Confidence: medium (reviewed logic and tests closely, did not run them).

Security

  • None found. Probes deliberately use credentials: "omit", go through the browser's own fetch (so CORS/mixed-content behavior mirrors what a real recipient would see), respect a bounded timeout/probe cap, and never surface the author's credentials. Confidence: medium-high.

Performance

  • None found. Probing is deduplicated by origin/target, capped at 16 distinct targets, run in parallel with a 6s per-request timeout, and skipped entirely for anything already settled without the network. Confidence: high.

Quality

  • share.readinessAllReachable / share.readinessTruncated (en.json, and all 18 translated catalogs) are called with a count option but don't use this codebase's established key_one/key_other i18next pluralization convention (documented in docs/i18n.md, used elsewhere in the same file, e.g. share.statusFound_one/_other). Instead they use a manual "(s)" suffix, which reads wrong for count === 1 and can't be corrected per-locale (confirmed ru.json carries the same broken "источник(ов)" hack). Posted inline. Confidence: medium-high.
  • Minor: ShareReadinessReport.probeCount is documented as "distinct targets actually requested," but the readinessTruncated copy calls it "source(s)" — since multiple layers can share one deduped probe target, the count shown to the author may not line up with the number of layer rows they'd expect. Confidence: low (cosmetic, not verified against real usage patterns).
  • Minor: summarizeShareSources groups all of a layer's problem references under one key and keeps only the first reference at the max severity when there's a tie, discarding the url/field of any other references at that same severity. Not incorrect, just a small loss of detail in the surfaced example URL. Confidence: low.

CLAUDE.md

  • New user-facing strings correctly go through t() and are translated into all 18 locale catalogs with complete key sets — aside from the pluralization issue noted above. No CSP/tile-host allowlist changes were needed since the probe targets are arbitrary user-referenced hosts already covered by the existing permissive https: CSP rule. Confidence: medium-high.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1812/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1812/demo/
Commit f172cfc

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

- Share one deadline across a target's HEAD attempt and its ranged-GET
  retry (CodeRabbit). Each attempt built its own `AbortSignal.timeout`, so
  a slow host that refuses HEAD could spend the budget twice and take 12s
  against a documented 6s per-target limit.
- Assert the retry's method and `Range: bytes=0-0` header in the
  HEAD-refusing test (CodeRabbit). The test only checked the final status,
  so dropping the range header, which would have the check pull down a
  whole multi-gigabyte COG on every such host, would not have failed it.
- Convert `share.readinessAllReachable` and `share.readinessTruncated` to
  i18next `_one`/`_other` plural keys across all 18 catalogs (claude-review).
  The manual "(s)" suffix rendered "All 1 data source(s)" and could not be
  fixed per language. Each locale now carries the CLDR categories it needs:
  the full six for Arabic, one/few/many/other for Russian, one/other for the
  rest, and `_other` alone for zh/ja/ko/th/id, which have a single category.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/i18n/locales/en.json`:
- Line 1221: Update the readinessTruncated_one entry first in
apps/geolibre-desktop/src/i18n/locales/en.json at lines 1221-1221 to remove the
ordinal, then align the corresponding singular entries in
apps/geolibre-desktop/src/i18n/locales/ar.json at lines 1359-1359,
apps/geolibre-desktop/src/i18n/locales/fa.json at lines 1215-1215, and
apps/geolibre-desktop/src/i18n/locales/hi.json at lines 1214-1214 with the
specified translations.

In `@apps/geolibre-desktop/src/i18n/locales/fa.json`:
- Line 1212: Update the Persian readinessAllReachable_other translation to use
count-neutral “all” wording, replacing the singular “هر {{count}} منبع داده”
phrasing while preserving the reachability meaning and count placeholder.

In `@apps/geolibre-desktop/src/i18n/locales/fr.json`:
- Line 1213: Update the French translation value for readinessNote to use
standard comparison syntax, replacing “différemment que chez vous” with
“différemment de ce qu'elles font chez vous” while preserving the rest of the
sentence.

In `@apps/geolibre-desktop/src/i18n/locales/id.json`:
- Around line 1175-1178: Update the Indonesian readinessAllReachable_other and
readinessTruncated_other translations to use count-neutral wording that reads
naturally for both singular and plural values, including replacing “Semua” with
a direct count-based phrase and using “sumber awal” instead of “sumber pertama.”

In `@apps/geolibre-desktop/src/i18n/locales/ja.json`:
- Line 1175: Update the Japanese translation value for
readinessAllReachable_other so the recipient is the subject with 受信者がアクセスできる,
while preserving the count placeholder and the existing meaning.

In `@apps/geolibre-desktop/src/i18n/locales/ru.json`:
- Line 1286: Update the Russian readinessTruncated_one translation to remove the
ordinal word “первый” while retaining the single {{count}} placeholder, so it
renders as “Проверен только {{count}} источник.” and preserves the English key’s
placeholder contract.

In `@apps/geolibre-desktop/src/lib/share-readiness.ts`:
- Line 484: Update the probe request in probeTarget() to use redirect: "error"
instead of following redirects, preventing classified or origin probes from
reaching unclassified destinations. Do not add redirect-following behavior
unless every redirect URL is reclassified before the next request.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e4610fdd-0223-4d31-bdb9-49d262b8b703

📥 Commits

Reviewing files that changed from the base of the PR and between 389508c and e370884.

📒 Files selected for processing (20)
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/share-readiness.ts
  • tests/share-readiness.test.ts

Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/fa.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/fr.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/id.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/ja.json Outdated
Comment thread apps/geolibre-desktop/src/i18n/locales/ru.json Outdated
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx:194embeddedLayerIds is derived purely from isEmbeddableLocalVectorLayer, but the actual Share upload can still fail to embed such a layer (unreadable/streamed source data, or the vector control not yet initialized), leaving it with neither a URL nor embedded data. The readiness check will have already skipped it as "embedded," so it silently misses exactly the failure mode the feature exists to catch. Medium confidence; narrow edge case.

Security

  • None found. Private/local hosts are excluded from network probing before any fetch happens, credentials are explicitly omitted from probe requests, and the probe budget/timeout bound the network surface. No secrets or unsafe input handling observed.

Performance

  • None found. Probing is capped at 16 distinct, de-duplicated targets, runs concurrently, uses a 6s per-target timeout, and tile templates collapse to their origin to avoid redundant/expensive requests.

Quality

  • apps/geolibre-desktop/src/lib/share-readiness.ts:500 — the "probe-budget" reason is reused for three distinct causes (budget cap, user-triggered abort, fetch unavailable); currently harmless since the UI shows generic copy for any unchecked status, but could mislead future consumers of the field. Low confidence, cosmetic.
  • apps/geolibre-desktop/src/lib/share-readiness.ts:205isPrivateHostname doesn't cover the CGNAT range 100.64.0.0/10; minor completeness gap, not incorrect. Low confidence.

CLAUDE.md

  • No violations found. New user-facing strings go through t()/i18next as required, all 18 locale catalogs were updated with keys matching en.json's structure (with correct locale-specific plural forms, e.g. Arabic's six categories and Russian's one/few/many/other), and the new module/tests follow the repository's existing patterns (e.g. reuse of redactUrlCredentials/isCredentialFieldName/isEmbeddableLocalVectorLayer rather than re-deriving logic, and use of the existing AbortSignal.any/AbortSignal.timeout/fetch-error.ts conventions already used elsewhere in the codebase).

giswqs added 2 commits August 9, 2026 20:30
…-readiness-check

# Conflicts:
#	apps/geolibre-desktop/src/i18n/locales/ar.json
#	apps/geolibre-desktop/src/i18n/locales/de.json
#	apps/geolibre-desktop/src/i18n/locales/es.json
#	apps/geolibre-desktop/src/i18n/locales/fr.json
#	apps/geolibre-desktop/src/i18n/locales/hi.json
#	apps/geolibre-desktop/src/i18n/locales/id.json
#	apps/geolibre-desktop/src/i18n/locales/it.json
#	apps/geolibre-desktop/src/i18n/locales/ja.json
#	apps/geolibre-desktop/src/i18n/locales/ka.json
#	apps/geolibre-desktop/src/i18n/locales/ko.json
#	apps/geolibre-desktop/src/i18n/locales/nl.json
#	apps/geolibre-desktop/src/i18n/locales/pt.json
#	apps/geolibre-desktop/src/i18n/locales/ru.json
#	apps/geolibre-desktop/src/i18n/locales/th.json
#	apps/geolibre-desktop/src/i18n/locales/tr.json
#	apps/geolibre-desktop/src/i18n/locales/zh.json
- Drop the ordinal from the singular truncation message (CodeRabbit).
  "Only the first 1 source was checked" reads as a miscount; the `_one`
  form now says "Only {{count}} source was checked", and ar, fa, hi, ka,
  ru, and tr drop their ordinal the same way.
- Make the Indonesian forms count-neutral (CodeRabbit). Indonesian has one
  plural category, so `_other` also renders at count 1.
- Fix three wordings CodeRabbit flagged: Persian used "each" where the
  message means "all", Japanese made the source rather than the recipient
  the subject of access, and the French note used the non-standard
  "différemment que" comparison.
- Give a caller-cancelled probe its own `aborted` reason (claude-review).
  It shared `probe-budget` with "past the cap" and "no fetch available",
  which is invisible today but misleading if the reason ever reaches
  diagnostics.
- Recognize the RFC 6598 carrier-grade NAT range 100.64.0.0/10 as private
  (claude-review), so a service reachable only there is pre-classified
  rather than reported as a generic CORS failure.
- Document the one case where `embeddedLayerIds` over-trusts the publish
  path (claude-review): the predicate says a layer *can* be embedded, but
  data the upload cannot read back is dropped, and such a layer would ship
  with neither a URL nor features after this check cleared it. Settling it
  would mean a DuckDB export of every local vector layer on dialog open,
  which is the cost this check exists to avoid.

Declined: switching the probe to `redirect: "error"`. Left for discussion
on the thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/geolibre-desktop/src/i18n/locales/es.json (3)

1227-1227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the CORS explanation.

Line 1227 makes the browser the subject of no envía. A browser does not send the response CORS headers. The host sends them.

Use wording such as el host no envía encabezados CORS.

Suggested wording
-    "readinessReasonCors": "Un navegador no puede acceder a este host: no envía encabezados entre orígenes, o no está disponible.",
+    "readinessReasonCors": "Un navegador no puede acceder a este host: el host no envía encabezados CORS o no está disponible.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/es.json` at line 1227, Update the
Spanish translation value for readinessReasonCors so the host, rather than the
browser, is identified as the sender of the CORS response headers; preserve the
existing meaning and use wording equivalent to “el host no envía encabezados
CORS.”

1199-1199: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use natural Spanish for count-bearing messages.

These strings can produce incorrect or awkward output:

  • Line 1199 uses campo(s).
  • Line 3149 can render 1 teselas.
  • Line 3210 can render 1 de 1 imágenes.
  • Lines 3335-3336 can render 1 elementos.

Use count-neutral wording, or matching plural keys when the English catalog defines them.

Suggested wording
-    "credentialsRemoved": "No se incluyeron {{count}} campo(s) de credenciales. Los destinatarios deben proporcionar sus propias credenciales o usar una referencia intermediada.",
+    "credentialsRemoved": "Se omitieron campos de credenciales ({{count}} en total). Los destinatarios deben proporcionar sus propias credenciales o usar una referencia intermediada.",
-    "cellCount": "{{count}} teselas en la vista",
+    "cellCount": "Teselas en la vista: {{count}}",
-    "showing": "Mostrando {{shown}} de {{total}} imágenes.",
+    "showing": "Imágenes mostradas: {{shown}} de {{total}}.",
-    "showing": "Mostrando {{count}} elementos.",
+    "showing": "Elementos mostrados: {{count}}.",
-    "showingOfMatched": "Mostrando {{count}} de {{matched}} elementos.",
+    "showingOfMatched": "Elementos mostrados: {{count}} de {{matched}}.",

Based on learnings, use count-neutral wording when a single key receives numeric interpolations unless the English catalog defines matching plural variants.

Also applies to: 3149-3149, 3210-3210, 3335-3336

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/es.json` at line 1199, Update the
affected Spanish locale entries, including credentialsRemoved and the messages
near the referenced tile, image, and element counts, to avoid gendered or plural
forms that become incorrect with numeric interpolation. Use count-neutral
Spanish wording unless the English catalog provides matching plural keys, in
which case align the Spanish singular and plural translations with those keys.

Source: Learnings


3299-3321: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the formal Spanish voice in the STAC messages.

These new strings switch to informal imperatives such as introduce, Elige, Selecciona, Mantén, Haz clic, and Deja. The surrounding catalog uses formal forms such as Introduzca, Elija, Seleccione, Haga clic, and Deje.

Use one form consistently.

Also applies to: 3347-3348

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/es.json` around lines 3299 - 3321, The
STAC strings in the Spanish locale use informal imperatives inconsistently with
the catalog’s formal voice. Update the affected entries, including the
additional strings around the referenced later entries, to use formal forms such
as “Introduzca”, “Elija”, “Seleccione”, “Mantenga”, “Haga clic” and “Deje”,
while preserving their existing meanings.
apps/geolibre-desktop/src/i18n/locales/it.json (1)

1227-1227: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the CORS explanation.

The Italian text says that the browser “does not send cross-origin headers.” The service must return the required CORS response headers. Change the text to state that the host does not allow cross-origin requests or does not return the required headers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/it.json` at line 1227, Update the
Italian translation value for readinessReasonCors to state that the host does
not allow cross-origin requests or does not return the required CORS response
headers, replacing the inaccurate claim that the browser does not send
cross-origin headers.
apps/geolibre-desktop/src/i18n/locales/fr.json (1)

3149-3149: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct French number agreement in the changed count labels.

The single-string labels at Line [3149], Line [3210], and Lines [3335-3336] use fixed plural nouns after a count. French can display 1 tuiles, 1 images, or 1 éléments. Lines [5333-5334] use the same noun form in _one and _other, so plural counts display 2 réseau.

Use count-neutral labels for single-string keys. Use singular and plural nouns for diagnostics.countNetwork. Keep the noun consistent with the diagnostics description.

Based on learnings, single-key locale strings with {{count}} should use count-neutral wording when the reference key has no plural variants.

Proposed wording
-    "cellCount": "{{count}} tuiles dans la vue",
+    "cellCount": "Tuiles dans la vue : {{count}}",

-    "showing": "Affichage de {{shown}} images sur {{total}}.",
+    "showing": "Images affichées : {{shown}} sur {{total}}.",

-    "showing": "Affichage de {{count}} éléments.",
-    "showingOfMatched": "Affichage de {{count}} éléments sur {{matched}}.",
+    "showing": "Éléments affichés : {{count}}.",
+    "showingOfMatched": "Éléments affichés : {{count}} sur {{matched}}.",

-    "countNetwork_one": "{{count}} réseau",
-    "countNetwork_other": "{{count}} réseau",
+    "countNetwork_one": "{{count}} requête réseau",
+    "countNetwork_other": "{{count}} requêtes réseau",

Also applies to: 3210-3210, 3335-3336, 5333-5334

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/fr.json` at line 3149, Update the
French count labels at cellCount and the other affected single-string count keys
to use count-neutral nouns, avoiding fixed singular or plural forms after
{{count}}. In diagnostics.countNetwork, provide distinct singular and plural
wording in its _one and _other variants, keeping the noun consistent with the
diagnostics description.

Source: Learnings

apps/geolibre-desktop/src/i18n/locales/id.json (1)

5128-5128: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Indonesian grammar in the raster class-limit message.

Line 5128 uses Menerapkan as a verb before simbologi, so the sentence is not grammatical. Use a noun phrase instead.

Proposed fix
-    "tooManyClasses": "Menerapkan simbologi mendukung hingga {{max}} kelas.",
+    "tooManyClasses": "Penerapan simbologi mendukung hingga {{max}} kelas.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/i18n/locales/id.json` at line 5128, Update the
tooManyClasses translation in the Indonesian locale to use a grammatical noun
phrase before “simbologi” instead of the verb “Menerapkan,” while preserving the
existing {{max}} placeholder and message meaning.
♻️ Duplicate comments (1)
apps/geolibre-desktop/src/lib/share-readiness.ts (1)

482-523: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

redirect: "follow" still lets a probe reach an unclassified destination.

A prior review flagged that probeTarget() classifies only the initial target, then follows redirects with redirect: "follow", so a redirect can send the anonymous probe request to a host that was never classified as public/probeable. That comment does not show a confirmation reply (unlike the shared-deadline fix, which was confirmed addressed), and this line is unchanged in the current diff. Since the check's job is to verify what a recipient's browser would fetch, an uncontrolled redirect can silently probe a different, unvetted host.

Use redirect: "error" unless each redirect URL is re-classified before the next request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/lib/share-readiness.ts` around lines 482 - 523,
Update the request options in probeTarget so redirects use “error” instead of
“follow”, preventing the anonymous probe from reaching an unclassified
destination; retain the existing HEAD/GET retry and shared-deadline behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/i18n/locales/es.json`:
- Line 1227: Update the Spanish translation value for readinessReasonCors so the
host, rather than the browser, is identified as the sender of the CORS response
headers; preserve the existing meaning and use wording equivalent to “el host no
envía encabezados CORS.”
- Line 1199: Update the affected Spanish locale entries, including
credentialsRemoved and the messages near the referenced tile, image, and element
counts, to avoid gendered or plural forms that become incorrect with numeric
interpolation. Use count-neutral Spanish wording unless the English catalog
provides matching plural keys, in which case align the Spanish singular and
plural translations with those keys.
- Around line 3299-3321: The STAC strings in the Spanish locale use informal
imperatives inconsistently with the catalog’s formal voice. Update the affected
entries, including the additional strings around the referenced later entries,
to use formal forms such as “Introduzca”, “Elija”, “Seleccione”, “Mantenga”,
“Haga clic” and “Deje”, while preserving their existing meanings.

In `@apps/geolibre-desktop/src/i18n/locales/fr.json`:
- Line 3149: Update the French count labels at cellCount and the other affected
single-string count keys to use count-neutral nouns, avoiding fixed singular or
plural forms after {{count}}. In diagnostics.countNetwork, provide distinct
singular and plural wording in its _one and _other variants, keeping the noun
consistent with the diagnostics description.

In `@apps/geolibre-desktop/src/i18n/locales/id.json`:
- Line 5128: Update the tooManyClasses translation in the Indonesian locale to
use a grammatical noun phrase before “simbologi” instead of the verb
“Menerapkan,” while preserving the existing {{max}} placeholder and message
meaning.

In `@apps/geolibre-desktop/src/i18n/locales/it.json`:
- Line 1227: Update the Italian translation value for readinessReasonCors to
state that the host does not allow cross-origin requests or does not return the
required CORS response headers, replacing the inaccurate claim that the browser
does not send cross-origin headers.

---

Duplicate comments:
In `@apps/geolibre-desktop/src/lib/share-readiness.ts`:
- Around line 482-523: Update the request options in probeTarget so redirects
use “error” instead of “follow”, preventing the anonymous probe from reaching an
unclassified destination; retain the existing HEAD/GET retry and shared-deadline
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f9b949e2-a6d2-4a56-aba3-c773b4795594

📥 Commits

Reviewing files that changed from the base of the PR and between e370884 and a4b2bcb.

📒 Files selected for processing (20)
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fa.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/share-readiness.ts
  • tests/share-readiness.test.ts
💤 Files with no reviewable changes (3)
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json

Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Code review

I reviewed share-readiness.ts (the classification/probing core), ShareProjectDialog.tsx (wiring), the new test suite, the en.json/other locale additions, and the packages/core export change. The implementation is well-structured, deliberately reuses isCredentialFieldName/redactUrlCredentials/isEmbeddableLocalVectorLayer from the publish path so classification can't drift from what the upload actually does, and the 23 new test cases cover the classification and probing logic (HEAD→ranged-GET fallback, status mapping, 5xx non-blame, budget cap, opaque-rejection handling, no-fetch fallback) well. probeTarget's "never throws" guarantee holds up, the abort/timeout wiring in the dialog effect is correct, and the credential/private-host/severity-ranking logic all checked out against the actual source it mirrors.

Bugs

  • None found with high confidence. Two low-confidence, low-impact issues posted inline:
    • share-readiness.ts:173MAX_FIELD_SCAN_DEPTH (6) doesn't match the real redaction pass's MAX_REDACT_DEPTH (12) in credentials.ts, so a credential field nested 6–11 levels deep would be silently stripped by the real upload but not flagged here. Unlikely in practice given how shallow layer configs are. (Confidence: low)
    • ShareProjectDialog.tsx:211 — the readiness effect depends on t, which can get a new reference on a language switch, causing the probe to needlessly re-run mid-dialog. Minor UX/perf nit, not a correctness issue. (Confidence: low)

Security

  • No issues found. Probes use credentials: "omit", go through the browser's own fetch (so CORS is genuinely respected), and private/internal hostnames are excluded from probing before any request is made. No raw HTML injection of url/label values — all rendered as JSX text content.

Performance

  • No issues found. Probing is capped at 16 distinct targets, deduplicated by origin for tile templates, run in parallel with a 6s per-target timeout, and a HEAD-refusing host only costs one byte via ranged GET rather than a full download.

Quality

  • No significant issues. Naming, module docs, and the split between pure classification (collectShareSources) and I/O (probeShareSources) are clear and testable. The dedup-by-worst-status aggregation and severity ordering are consistent and match their test coverage.

CLAUDE.md

  • No violations found. New user-facing strings all go through t(), en.json is the source of truth with the other 17 locales updated consistently (languages without a distinct plural form correctly omit _one variants), and no ml-/left- physical Tailwind utilities were introduced.

- Scan for credential fields exactly as deep as the redaction pass does
  (claude-review). `MAX_FIELD_SCAN_DEPTH` was 6 while `MAX_REDACT_DEPTH` is
  12, so a credential nested between those depths would be stripped by the
  upload but reported as reachable here. `MAX_REDACT_DEPTH` is now exported
  from core and used directly, since the point of this module is to reuse
  the redaction rules rather than keep a second copy of them.
- Stop re-probing when the UI language changes (claude-review). The effect
  depended on `t` only to label the two project-level rows, so react-i18next
  handing out a new `t` on a language switch re-issued every network probe.
  The check no longer takes labels at all: it reports project-level rows
  with an empty `label` plus their `field`, and the dialog translates one at
  render time. The effect now depends on `[open, hasToken]`.

Verified in the browser that the basemap row still labels correctly, and
localizes (`Hintergrundkarte` under `?lang=de`).
@giswqs
giswqs merged commit 925d5cc into main Aug 10, 2026
23 of 24 checks passed
@giswqs
giswqs deleted the feat/issue-1671-share-readiness-check branch August 10, 2026 00:52
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts
Comment thread apps/geolibre-desktop/src/lib/share-readiness.ts
@github-actions

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • carriesOwnData (share-readiness.ts:374-381) doesn't recognize array-shaped source.data, which is exactly how non-GeoJSON deck.gl visualization layers (arc/heatmap/hexagon built from CSV rows, layer.type === "deckgl-viz") store their fully-embedded data (createDeckVizStoreLayer in deckgl-viz/store-layer.ts). Since isPlainObject excludes arrays and layer.geojson is unset for these layers, they fall through to layerReferences, find no reference, and get incorrectly flagged "no-source" (or "local-file" if sourcePath still holds the original local CSV path) even though the data ships entirely inside the project. High confidence — traced through the actual layer-creation and rendering code.

Performance

  • The HEAD-refusal fallback in probeTarget (share-readiness.ts:507) adds a Range: bytes=0-0 header to the retry GET. Range isn't a CORS-safelisted header, so for cross-origin targets this forces a preflighted request, whereas the initial HEAD doesn't need one. A host that rejects HEAD (landing it in the retry path) but supports plain cross-origin GET without preflight support could be reported as "blocked"/"cors" even though it would actually render fine. Advisory-only impact (doesn't block sharing), but could produce a misleading warning. Medium confidence — reasoned from CORS preflight semantics, not confirmed against a real host.

Security

  • Nothing found. Credential handling (hasCredentialField's recursion depth) correctly mirrors redactProjectCredentials's MAX_REDACT_DEPTH/traversal shape in packages/core/src/credentials.ts, including the exact fields (layer.source, layer.metadata) that redaction sweeps. Probes withhold credentials (credentials: "omit") and never probe private/local hosts (short-circuited before any fetch).

Quality

  • No significant issues. The module is well-structured (pure classification vs. network probing split), the private-hostname / template-collapsing / dedup logic all checked out against their test coverage, and the new MAX_REDACT_DEPTH export correctly reuses credentials.ts's constant rather than the unrelated same-named constant in layer-library.ts.

CLAUDE.md

  • Compliant: all new UI strings go through t(), translated across all 18 locales; new Tailwind classes in ShareProjectDialog.tsx are all direction-agnostic (no physical ml-/left-/text-left usage), consistent with the RTL logical-utilities requirement.

giswqs added a commit that referenced this pull request Aug 10, 2026
…s check (#1823)

* fix(share): do not flag embedded deck.gl rows as unloadable

Two follow-ups to the share-readiness check in #1812, from review comments
that landed after that PR merged.

A non-GeoJSON deck.gl visualization (arc, heatmap, hexagon built from a CSV)
keeps its rows in `source.data` as an array. `isPlainObject` excludes arrays,
so `carriesOwnData` missed those layers, they fell through to the reference
walk, and a layer whose data travels inside the project file was reported as
"no source" or, when `sourcePath` still held the original CSV name, as a
local file. An array `data` now counts as embedded, like an inline
FeatureCollection. Only a string `data` is a URL.

Separately, a rejected ranged GET no longer condemns the host. `Range` is
CORS-safelisted only for a simple byte range, and an older webview may
preflight it and get no matching `Access-Control-Allow-Headers` back. The
HEAD that preceded the retry already proved the host answers and lets this
origin read the response, so a rejection there is about the ranged request
rather than the host: the probe falls back to the HEAD's verdict instead of
reporting "a browser cannot fetch this host" for a host whose plain GET a
renderer would fetch fine.

Refs #1671.

* Assert the ranged GET ran in the fallback tests

Both fallback tests rejected every non-HEAD request but never checked that a
GET was attempted, so they would have kept passing if the retry were dropped
entirely: a bare HEAD 405 already reads as reachable and a bare 403 as
credentialed. They now record each attempt through a shared helper and assert
HEAD followed by GET with `Range: bytes=0-0`. Verified by deleting the retry
locally, which takes the suite from 26 passing to 3 failing instead of 1.
giswqs added a commit that referenced this pull request Aug 10, 2026
…ipients cannot load (#1812)

* feat(share): warn when a shared project references data recipients cannot load

A `.geolibre.json` is mostly references. The publish path embeds local
vector data, but every tile template, COG, PMTiles endpoint, OGC service,
and hosted feature service stays a URL. So a project can upload cleanly
and still draw nothing for the recipient: the service wanted a token the
author holds and the upload strips, the host sends no cross-origin
headers so it works in the desktop app and fails in the browser viewer,
or the reference never left the author's machine. The author finds out
when someone tells them the map is empty, if they tell them.

Add a pre-flight readiness check that runs when the Share dialog opens.

`share-readiness.ts` splits it in two so the classification stays pure
and testable. `collectShareSources` walks every place a layer can hide a
reference (`source.url`/`data`/`tiles`/`urls`/`baseUrl`/`arcgisQueryUrl`,
`metadata.originalUrl`/`tileUrl`/`localFilePath`/`localBytesUrl`,
`sourcePath`) plus the basemap style and plugin manifests, and settles
what needs no network: a filesystem path, a private-network or
single-label host, a URL whose credential `redactUrlCredentials` will
strip, a populated credential field, a query-backed layer that names no
source at all. `probeShareSources` resolves the rest with one anonymous
HEAD per distinct target (a ranged GET retry for hosts that refuse HEAD),
capped and short-timed out.

Two things the classification reuses rather than re-derives: which layers
the publish path embeds comes from `isEmbeddableLocalVectorLayer`, and
whether a credential survives the upload comes from the same redaction
rules the upload applies. A tile template collapses to its origin before
probing, because substituting a nominal 0/0/0 tile 404s on any service
whose data starts deeper and would report a healthy basemap as missing.

The probes deliberately go through the browser's `fetch`, not the desktop
app's native HTTP bypass, so a cross-origin rejection surfaces as one
instead of being masked by a request that is not subject to CORS. That is
what the recipient's browser will do.

The dialog lists one row per layer with its worst verdict, a
plain-language reason, and a fix. It never gates the Share button: an
author sharing an intranet map with intranet colleagues is doing the
right thing and should not have to fight a warning.

Fixes #1671

* Address review feedback

- Share one deadline across a target's HEAD attempt and its ranged-GET
  retry (CodeRabbit). Each attempt built its own `AbortSignal.timeout`, so
  a slow host that refuses HEAD could spend the budget twice and take 12s
  against a documented 6s per-target limit.
- Assert the retry's method and `Range: bytes=0-0` header in the
  HEAD-refusing test (CodeRabbit). The test only checked the final status,
  so dropping the range header, which would have the check pull down a
  whole multi-gigabyte COG on every such host, would not have failed it.
- Convert `share.readinessAllReachable` and `share.readinessTruncated` to
  i18next `_one`/`_other` plural keys across all 18 catalogs (claude-review).
  The manual "(s)" suffix rendered "All 1 data source(s)" and could not be
  fixed per language. Each locale now carries the CLDR categories it needs:
  the full six for Arabic, one/few/many/other for Russian, one/other for the
  rest, and `_other` alone for zh/ja/ko/th/id, which have a single category.

* Address second review round

- Drop the ordinal from the singular truncation message (CodeRabbit).
  "Only the first 1 source was checked" reads as a miscount; the `_one`
  form now says "Only {{count}} source was checked", and ar, fa, hi, ka,
  ru, and tr drop their ordinal the same way.
- Make the Indonesian forms count-neutral (CodeRabbit). Indonesian has one
  plural category, so `_other` also renders at count 1.
- Fix three wordings CodeRabbit flagged: Persian used "each" where the
  message means "all", Japanese made the source rather than the recipient
  the subject of access, and the French note used the non-standard
  "différemment que" comparison.
- Give a caller-cancelled probe its own `aborted` reason (claude-review).
  It shared `probe-budget` with "past the cap" and "no fetch available",
  which is invisible today but misleading if the reason ever reaches
  diagnostics.
- Recognize the RFC 6598 carrier-grade NAT range 100.64.0.0/10 as private
  (claude-review), so a service reachable only there is pre-classified
  rather than reported as a generic CORS failure.
- Document the one case where `embeddedLayerIds` over-trusts the publish
  path (claude-review): the predicate says a layer *can* be embedded, but
  data the upload cannot read back is dropped, and such a layer would ship
  with neither a URL nor features after this check cleared it. Settling it
  would mean a DuckDB export of every local vector layer on dialog open,
  which is the cost this check exists to avoid.

Declined: switching the probe to `redirect: "error"`. Left for discussion
on the thread.

* Address third review round

- Scan for credential fields exactly as deep as the redaction pass does
  (claude-review). `MAX_FIELD_SCAN_DEPTH` was 6 while `MAX_REDACT_DEPTH` is
  12, so a credential nested between those depths would be stripped by the
  upload but reported as reachable here. `MAX_REDACT_DEPTH` is now exported
  from core and used directly, since the point of this module is to reuse
  the redaction rules rather than keep a second copy of them.
- Stop re-probing when the UI language changes (claude-review). The effect
  depended on `t` only to label the two project-level rows, so react-i18next
  handing out a new `t` on a language switch re-issued every network probe.
  The check no longer takes labels at all: it reports project-level rows
  with an empty `label` plus their `field`, and the dialog translates one at
  render time. The effect now depends on `[open, hasToken]`.

Verified in the browser that the basemap row still labels correctly, and
localizes (`Hintergrundkarte` under `?lang=de`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Share-readiness check: warn when a shared project references data recipients cannot load

2 participants