Skip to main content
All insights

2026-05-21DataMesh Consulting

21 May — iOS v1.1 to the App Store, AfDB detail-page extractor, dashboard site reorder

A heavy ship day across three surfaces. iOS v1.1 went to App Store review — version bump, full AppIcon asset catalog, IAP compliance, plus three behavioural fixes (honest offline banner, rich SwiftData cache, detail-screen refetch after deep-link). The African Development Bank extractor was rewritten as a URL-dispatching Step-0 that handles both listing pages and per-project detail pages, with terminal-status pruning so closed/cancelled tenders don't keep getting re-extracted. And the operator dashboard's Sites tab got drag-and-drop reorder + enable/disable controls, replacing the JSON-edit-and-redeploy workflow.

iOS v1.1 — submitted to App Store review

A bundle of items had accumulated since v1.0:

  • Version bump to 1.1, build 17.
  • AppIcon asset catalog properly populated. The previous
build was shipping with the 1024×1024 source-only icon set, relying on Xcode 14+'s auto-scaling. That works for development but the App Store binary validator wants the standard size set explicitly. Now all required sizes (40, 60, 76, 80, 87, 114, 120, 152, 167, 180, 1024) are in AppIcon.appiconset.
  • IAP compliance — Apple's review notes on v1.0
flagged that the credits purchase wasn't going through StoreKit. Now wired through Product.purchase() with receipt validation against the backend's /v1/billing endpoint. The non-IAP web-purchase path still exists for direct sign-ups via the web portal, but inside the iOS app it's IAP-only per Apple's guidelines.

Plus three behavioural fixes in the same submission:

  • Honest offline banner — the previous version showed
"Offline" on any error including server 5xx and decode failures. Wi-Fi was fine but users thought they had no internet. Now distinguishes URLError offline-class codes ("Offline — showing cached data") from other errors ("Couldn't refresh — showing cached data").
  • Rich SwiftData cache — the cache schema was missing
half the Tender fields, so the offline view dropped to bare title + organisation. Now persists fitScore, deadline, recommendation, riskLevel, the full description, and the matchedKeywords so the offline experience is closer to online.
  • Detail-screen refetch — opening a tender via a deep
link (push notification or shared URL) was rendering whatever the cached version held. Now triggers a background fetch for the live tender state and patches the view when it returns. Solves the "I bookmarked this on web and the app still shows it as unbookmarked" feedback.

African Development Bank — URL-dispatching extractor

The previous AfDB extractor was listing-only — it scraped the procurement-notices feed and stopped. Description, CPV codes, contact info, and value all stayed null because they live on the per-project detail pages.

The rewrite dispatches on URL pattern at job-time:

  • https://www.afdb.org/.../procurement-notices?... → listing
parser (paginated, returns notice URLs).
  • https://www.afdb.org/.../project/... → detail parser
(description, CPV mapping, contact block, deadline, estimated value in the local currency, terminal-status detection).

Terminal-status pruning is the new behaviour:

  • If the detail page shows the notice as "Award notice",
"Cancelled", "Closed", we mark the tender ARCHIVED and remove it from active listings.
  • The next listing-poll won't re-fetch the detail page
because the dedup index says we already have it AND its status is terminal.
  • This stops a long-running waste pattern where closed
tenders were getting re-fetched every poll cycle.

Coverage delta: AfDB went from ~600 tenders with mostly-null fields to ~480 tenders with full enrichment (the drop is explained by terminal-status pruning sweeping out years of old award notices). Average CPV-code coverage on AfDB notices: 78%.

Dashboard Sites tab — drag-and-drop reorder + enable/disable

The Sites tab in the operator dashboard had been read-only. Reordering or disabling a site meant editing backend/scripts/seed-sites.js, running the seed against prod, and redeploying. Slow. Risky on a Saturday.

Three new affordances on the Sites page:

  • Drag handle on each row reorders by priority. Lower
priority number = scraped sooner each cycle. State persists via PATCH /v1/admin/sites/:id { priority }.
  • Enable/disable toggle flips isActive. Inactive
sites are excluded from the next scrape cycle without losing their history (so we can re-enable later).
  • Optimistic UI so the toggle/reorder feels instant; if
the backend rejects (e.g. concurrent edit conflict), the UI reverts and shows the error.

Underlying API mutations all run through the standard admin-auth guard. No new privileged endpoints.

Other items

  • /tenders/search response-shape fix — the backend
endpoint had been switched from a bare [Tender] array to a { data, totalCount, ... } paginated envelope. iOS was decoding the bare-array shape and silently failing on the new envelope, showing "No results" even when matches existed. iOS now tries both shapes (bare first for compat, then envelope fallback). Backend will also accept either query format.
  • Saved-but-unmatched tenders in /tenders/matches
if a user bookmarked a tender that later didn't make the top-50 AI-match feed (or never produced a TenderMatch row), it was invisible on the main page but still showed on the Saved tab. The Matches feed now augments with saved-but-unmatched rows so the count is consistent across screens.
  • SEO: redirects in generateMetadata — GSC was
flagging canonical mismatches on a handful of legacy URLs because the redirect was happening in middleware after the page resolved its canonical. Moving the redirect logic into generateMetadata makes the canonical match the destination URL.

What's next

  • Wait on App Store review. The 1.1 submission has new IAP
metadata and the AI consent disclosure update, both of which may extend review time beyond the usual 24-48h.
  • Tomorrow: the extractor wave continues. ADB and UNGM
are queued.
Methodology: drawn from the week ending 2026-05-21 tender corpus. Tender data sourced from public procurement portals worldwide; see our methodology for the extraction pipeline.