Skip to content

v2.0.4: scheduler retry upgrade, local storage flag, Uzbek translations - #83

Open
Victor1Ja wants to merge 31 commits into
release-candidate/2.0.4from
release-candidate-test-2.0.4
Open

v2.0.4: scheduler retry upgrade, local storage flag, Uzbek translations#83
Victor1Ja wants to merge 31 commits into
release-candidate/2.0.4from
release-candidate-test-2.0.4

Conversation

@Victor1Ja

@Victor1Ja Victor1Ja commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Three of the v2.0.4 items, plus a build fix the branch needed.

1. Scheduler retry upgrade (20d725d)

Scheduled tests retried 3 times, 15 minutes apart — about 45 minutes of coverage inside a 4-hour window. If connectivity came back later in the window, the slot was lost.

The app now retries for the whole window, paced by why the attempt failed:

  • No network → retry every minute. These retries generate no test traffic, so they're free and catch the connection as soon as it's back. getNetInfo() also gets a navigator.onLine pre-check, so offline ticks stop hitting the IP-info services.
  • Test failed with the network up → exponential backoff min(60s × 1.2^n, 10 min), since each attempt costs real bandwidth. ~30 attempts per window instead of ~240 at a flat minute.

The semaphore now persists retryAttempts, backoffLevel (reset when the network returns after a no-network failure) and lastFailReason. Pre-2.0.4 semaphores without these fields are handled.

Bug found along the way: when fully offline, getNetInfo() throws — its geojs fallback fails too. That exception escaped decide() and was swallowed by watch(), so the no-network path never reached the rescheduling logic at all. It retried every minute by accident, without counting attempts. Now caught and treated as no-network.

schedule.service.spec.ts had only a "should be created" and was rewritten: 13 specs covering the fixed offline pace, the 1.2^n growth, the cap, the recovery reset, success, window expiry, the clamp to the window end, and legacy semaphores.

2. Local test storage flag (ae5d6ba)

Measurements whose realtime upload failed were already queued in IndexedDB and re-sent by the periodic sync, but once they reached the backend they were indistinguishable from realtime uploads. The payload now carries:

  • upload_failedfalse on realtime uploads; the copy queued in IndexedDB is saved with true, so the sync delivers it already flagged and needed no changes of its own.
  • scheduled_slot'A' | 'B' | 'C' for slot tests, 'startup' for the daily launch test, null for manual runs.
  • scheduled_at — the originally planned run time (the semaphore keeps it separately from choice, which moves with every retry).

The sync payload also stops leaking IndexedDB bookkeeping fields (id, status, createdAt) — the old code stripped isSynced, which these records never had.

Backend counterpart: unicef/giga-meter-backend#347. The columns are additive, so this can ship before or after it.

3. Uzbek translations (f405083)

35 revised strings from the reviewed translation sheet (186 rows keyed by dotted path):

  • Terminology and typo fixes — IDIDsi agreement, ulanishinternet where the English says connectivity, MaktabinggizMaktabingiz.
  • Strings still in English (ISP, Open Database License, Dashboard) now translated.
  • Verified: all 186 sheet keys resolve against the JSON, the key set is unchanged, and every {{placeholder}} and HTML tag still matches en.json.

⚠️ releaseNotes.2.0.2.title is deliberately not applied: the sheet holds the old translation concatenated with a reworded one (... tajribasi - Yaxshilangan ...), which would render as a doubled title. Left at its current value, pending confirmation from the translator.

4. Spec import fixes (1211351)

indexed-db.service.spec.ts imported a LocalStorageService from a file that no longer exists, and invalidlocation.page.spec.ts imported SchoolnotfoundPage from a page that exports InvalidLocationPage. Both broke compilation of the whole karma suite, so nothing could be tested on this branch until they were fixed.

Testing

  • schedule.service.spec.ts: 13/13 green.
  • Full suite: the 18 pre-existing should create failures remain (TestBed setups missing providers, in components untouched here — they were hidden before, since the suite didn't compile). No new failures.
  • ng lint is broken repo-wide (.eslintrc.json can't load plugin:@angular-eslint/recommended-extra) — pre-existing, not addressed here.

Still to do before the release

  • Manual QA on staging for the scheduler (drop the network, block locate.measurementlab.net, reconnect) and end-to-end for the storage flag.
  • Version bump to 2.0.4, coordinated with the rest of the release.

Victor1Ja and others added 8 commits May 15, 2026 17:24
indexed-db.service.spec.ts imported LocalStorageService from a file that no
longer exists; invalidlocation.page.spec.ts imported SchoolnotfoundPage from
invalidlocation.page, which exports InvalidLocationPage. Both broke compilation
of the whole karma suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re type

Replaces the 3-retries-every-15-min cap (~45 min of coverage in a 4-hour
window) with a policy that retries until the test completes or the window
ends, paced by why the attempt failed:

- No network: retry every minute. These retries generate no test traffic,
  and getNetInfo() now gets a navigator.onLine pre-check so offline ticks
  don't hit the IP-info services either.
- Test failed with network up: exponential backoff min(60s * 1.2^n, 10 min),
  since each attempt consumes real bandwidth.

The semaphore now persists retryAttempts (total tries in the slot),
backoffLevel (the backoff exponent, reset when the network comes back after
a no-network failure) and lastFailReason. getNetInfo() exceptions - which
previously escaped decide() and skipped rescheduling entirely - are now
caught and treated as no-network.

Plan: project-memory/plans/0005-scheduler-retry-upgrade-v2.0.4.md (giga repo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measurements that fail the realtime upload already get queued in IndexedDB
and re-sent by the periodic sync, but on the backend they were
indistinguishable from realtime uploads. The payload now carries:

- upload_failed: false on realtime uploads; the copy queued in IndexedDB is
  saved with true, so the sync delivers it flagged without any sync changes.
- scheduled_slot: 'A' | 'B' | 'C' for slot tests, 'startup' for the daily
  launch test, null for manual runs.
- scheduled_at: the originally planned run time (the semaphore keeps it
  separately from choice, which moves with every retry).

The sync payload also stops leaking IndexedDB bookkeeping fields (id,
status, createdAt); the old code stripped isSynced, which measurement
records never had.

Plan: project-memory/plans/0006-local-test-storage-flag-v2.0.4.md (giga repo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The app bundled a modified copy of @m-lab/ndt7 0.0.6 (2022) under
src/assets/js/ndt/. The copy differed from upstream in two ways: it added
client_name=giga-meter to the metadata sent to M-Lab, and it removed the
Node-only polyfills (require('ws') etc.) that broke webpack builds.

Upstream 0.1.5 makes both edits unnecessary: the polyfills are gone and
config.metadata is the official way to send client_name. So this switches
to the package:

- @m-lab/ndt7 bumped 0.0.6 -> ^0.1.5 (it was already a dependency, unused).
- measurement-client imports the package and passes
  metadata: { client_name: 'giga-meter', client_version: app_version }.
- The worker files ship from node_modules via an angular.json assets glob;
  runtime paths are unchanged.
- Vendored ndt7.js and both workers deleted (NDT5 legacy files left alone).
- src/types/ndt7.d.ts declares the untyped package.

Error propagation is structurally identical between the copy and 0.1.5
(same throw on locate fetch failure, same 'Could not understand response'
string), so the locate-error retry classification keeps working; specs
cover the config passed to ndt7.test and the retry classification.

Also brings 0.1.5's improved timeouts: 10s to connect + 12s of test after
connecting, instead of 12s total.

Plan: project-memory/plans/0007-ndt7-npm-package-migration.md (giga repo)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
indexed-db.service.spec.ts imported LocalStorageService from a file that no
longer exists; invalidlocation.page.spec.ts imported SchoolnotfoundPage from
invalidlocation.page, which exports InvalidLocationPage. Both broke compilation
of the whole karma suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Silences the optimization-bailout warning the same way electron already is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies 35 revised strings from 'Translations GigaMeter_UZB and Rus.xlsx'
(186 rows keyed by dotted path). Most are terminology and typo fixes:
'ID' -> 'IDsi' agreement, 'ulanish' -> 'internet' where the English says
connectivity, 'Maktabinggiz' -> 'Maktabingiz', and strings that were left
in English ('ISP', 'Open Database License', 'Dashboard') now translated.

releaseNotes.2.0.2.title is deliberately NOT applied: the sheet holds the
old translation concatenated with a reworded one ('... tajribasi - Yaxshilangan
...'), which would render as a doubled title. Left at its current value
pending confirmation from the translator.

Verified: all 186 sheet keys resolve against the JSON (including the dotted
version keys under releaseNotes and the array items), key set unchanged, and
every {{placeholder}} and HTML tag still matches en.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Victor1Ja and others added 2 commits August 11, 2026 11:50
Standalone Node probe (no build, no Electron) covering the release
v2.0.4 item-3 research: network interfaces, gateway, DNS, VPN
inference, Wi-Fi, network stats/connections, OS, CPU, disk, memory
and elevation. Two passes to flag volatile attributes, per-call
timing, and JSON (raw + redacted) + CSV outputs. Probe outputs are
gitignored: the raw dump contains SSIDs, MACs, internal IPs and the
Windows username.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make probe-system-info.js requirable (exports main), tag output files
with the runtime (node vs electron-<version>), honor PROBE_OUT_DIR,
and add electron-probe-main.js to run the probe on Electron's embedded
Node — Artefacto 2 of plan 0008, unpackaged variant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Victor1Ja and others added 7 commits August 17, 2026 16:23
Automates the 8-step manual checklist of plan 0010 against a real backend,
replacing the dead Protractor scaffold (`ng e2e` pointed at Protractor 7 and
there was no runnable e2e at all).

Covered, in serial over one install (the checklist is a linear walk and the
state accumulates): clean start -> school registration -> first ndt7 test
(real, against M-Lab) -> realtime upload -> manual test -> scheduled slot ->
post-measurement UI -> restart persistence.

The plan 0006 fields (upload_failed, scheduled_slot, scheduled_at) are checked
in the DB column, not just in the POST payload (e2e/playwright/db.ts queries
Postgres through the container, so no new dependency).

Notable bits:
- Waiting for a real slot would take hours, so step 6 injects an expired
  semaphore for slot A and lets the scheduler's 60s tick pick it up, as it
  would in production. scheduledTesting has to be enabled first or
  getSemaphore() wipes the semaphore on every tick.
- Fixtures live here, not in giga-meter-backend: its seed-runner.ts and Spain
  seed only exist on the develop line, which tied the suite to that branch.
  e2e/seed/{seed.js,seed-spain.sql} are mounted into the container instead, so
  the suite runs against develop and staging alike.
- The compose sets DIRECT_DATABASE_URL: some branches declare `directUrl` in
  the Prisma datasource and the backend dies with P1012 without it.

Verified green (5 passed) against both backend lines: develop and staging,
each with the plan 0006 migration applied.

`npm run e2e:stg` runs the same spec against the real Azure staging, skipping
the DB assertions. Not exercised yet: it writes real data to a shared
environment.

Plan: project-memory/plans/0010-e2e-happy-path-test-rc-2.0.4.md (giga repo)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Item 2 of the v2.0.4 release plan. Mirrors matomo.service.ts: everything is
wrapped in try/catch so a failure here can never take the app down, and with
no configuration the service simply does nothing.

posthog-js is bundled rather than loaded from a CDN the way Matomo is. This is
a desktop app that spends real time offline — that is literally what it
measures — so the SDK needs to queue events until connectivity returns, and
bundling avoids pulling a remote script in under Electron's CSP.

Configuration (placeholders for now, real keys still to be pasted in):
- posthogKeyDev/Stg/Prod + posthogHost in _environment.prod.ts, with
  placeholders in the .example. The project API key is the Sentry-DSN
  equivalent: public and embedded in the build, never a personal API key.
- An empty key disables PostHog for that mode, so an unconfigured build sends
  nothing. That is also what keeps the e2e suite from talking to a real
  project.

Privacy choices worth flagging:
- Identity is the school's giga id — the same identifier already travelling
  with every measurement. No school name, Windows username, install path or IP.
- autocapture and capture_pageview are off; only explicit events are sent.
  Pageviews are captured manually because autocapture does not see hash routes.
- Session replay is off behind a flag. It records the user's screen and the
  scope is still being defined by Shilpa's research; in schools that is a
  privacy call, not a technical one.

Funnel events: app_started, registration_completed, measurement_uploaded,
measurement_queued_offline, measurements_synced, measurements_sync_failed. The
last three close the loop on the plan 0006 offline flag.

Verified: tsc clean, build OK, karma shows the same 18 failures / 40 passes
with and without this change (pre-existing baseline, no regressions), and the
Playwright e2e suite stays green (5 passed).

Plan: project-memory/plans/0004-release-v2.0.4-2026-08.md (giga repo)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the PostHog integration: the desktop shell now reports the one
thing neither the renderer nor the backend can see — the auto-update lifecycle.
A machine that fails to update stops sending measurements, so it silently
vanishes from the app_version adoption queries.

The main process gets no PostHog SDK or key of its own. posthog-node was
implemented first and dropped:

- Most of what it added was already covered. Main-process errors go to Sentry
  (@sentry/node), and version adoption comes from app_version on every
  measurement (project-memory/technical/APP_VERSION_ADOPTION_QUERIES.md).
- posthog-node does not persist its queue between sessions; posthog-js does, in
  localStorage. On a machine that spends hours offline, adding the node SDK
  would have made shell telemetry the *less* reliable half, and it forced a
  bounded flush on quit that risked hanging app shutdown.

So the main process emits `desktop_update_downloaded` / `desktop_update_failed`
on a `telemetry-event` IPC channel (exposed through the preload as
electronAPI.onTelemetryEvent) and the renderer publishes them with the key it
already has. Events are dropped if the window is not alive, which is acceptable:
update failures already reach Sentry independently.

tsc clean on both the main process and the renderer.

Plan: project-memory/plans/0004-release-v2.0.4-2026-08.md (giga repo)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both branches implemented PostHog independently (feat/posthog is not even based
on the 2.0.4 RC). Conflicts resolved to keep one implementation, per Victor:

Configuration
- Per-environment project API keys (prod/dev/stg) with a session-recording flag
  that stays off unless explicitly enabled, from this branch.
- The EU host default from feat/posthog is kept: without it posthog-js falls
  back to US, and school data should not leave the EU by omission.
- Persistence is 'localStorage+cookie' from feat/posthog: localStorage holds the
  offline queue, the cookie keeps the device's anon id stable if the renderer
  storage is cleared.
- init() now also bails out on the placeholder key from the example config, so a
  half-configured build sends nothing instead of sending to a bogus project.

Main process
- No posthog-node: electron/src/analytics.ts and the dependency are dropped, and
  app_launched / app_quit join the auto-update events on the existing
  'telemetry-event' IPC channel. posthog-js persists its queue in localStorage
  and survives restarts offline, which posthog-node would not — and this app is
  offline a lot.
- app_quit is emitted before myCapacitorApp.cleanup(), while the window is still
  alive; if the process dies before it flushes, the queued event ships on the
  next launch.
- The 'posthog-identity' IPC channel is gone with posthog-node: identity now
  lives entirely in the renderer.

Identity
- Both models are kept: identify() by GigaID (this branch) and the school as a
  PostHog *group* (feat/posthog). The group is what allows counting devices per
  school instead of collapsing them into a single person.

Events kept from both sides: app_started, registration_completed,
measurement_uploaded, measurement_queued_offline, measurements_synced,
measurements_sync_failed (this branch), app_launched, app_quit (feat/posthog),
plus the auto-update pair and manual $pageview.

Verified: ng build green, tsc clean on app sources and electron main, karma
shows the same 18 pre-existing "should create" failures as before the merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Victor1Ja and others added 10 commits August 24, 2026 16:20
Reverts b1b65e5 and c9b1796, restoring the vendored ndt7 client under
src/assets/js/ndt/.

The npm package is a black box from the app's point of view, and the
next commit needs to reach into the client itself: ndt7 exposes no
server-side wall clock, so capturing one means editing
discoverServerURLs. Owning the file again is the cheapest way to do
that without carrying a patched fork of the package.

Restores: ndt7.js and both workers, @m-lab/ndt7 back to ^0.0.6, the
angular.json assets glob and allowedCommonJsDependencies entry, and the
vendored import in measurement-client. Drops src/types/ndt7.d.ts, which
only existed to type the package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measurements are stamped with Date.now(), so the timestamp is only as
trustworthy as the clock of the machine running the test - and on these
machines it frequently is not.

ndt7 has no wall clock of its own: the measurement messages the server
sends during a test carry only ElapsedTime, relative to the start of the
test, and the browser WebSocket API does not expose the headers of the
handshake response. The one server clock within reach is the Date header
of the locate service response, which discoverServerURLs already fetches.
Date is CORS-safelisted, so it reads cross-origin with no change on
M-Lab's side.

ndt7.js reads that header, returns it as ServerTime alongside the URLs
and hands it to the download/upload complete callbacks. It is null when
the header is missing or unparseable, and on the config.server path,
which never contacts the locate service.

measurement-client stores it on the record as serverTimestamp, preferring
the download leg, and upload.service sends it as server_timestamp (ISO
8601, or null). The existing timestamp field is untouched: this is an
extra reference point, not a replacement.

Backend counterpart: unicef/giga-meter-backend PR adding the
server_timestamp column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The probe scripts mixed Spanish into console output, probe metadata and the
CSV the tracking spreadsheet is built from. Translate all of it so the
research artefacts are shareable with the wider team.

Text-only change: console messages, `group`/`attr`/`requiresAdmin` labels,
the availability/volatility values (`sí`/`parcial`/`vacío` -> `yes`/`partial`/
`empty`) and the CSV header. The row keys were renamed alongside their CSV
consumer (`disponible`/`ejemplo`/`volatil`/`notas` -> `available`/`sample`/
`volatile`/`notes`). No logic changes.

Note: the generated CSV/JSON columns and the `group` values change, so a
table already pasted into the UNICEF spreadsheet needs regenerating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Research probe: network & device info retrievable from the Windows client (plan 0008)
Fills the device columns the backend has accepted since giga-meter-backend#353
but that nothing ever populated — every row landed with NULL device_name,
device_model, device_manufacturer, app_build_number and sdk_version — and adds
the network context and Wi-Fi diagnosis from research plan 0008.

Main process:

- `device-context.ts` captures the volatile context the ticket asked for and no
  column covered: DNS servers, default gateway, connection type, VPN inference,
  IP family, rx/tx counters, CPU load, available memory and free disk.
- The `get-wifi-connections` handler now explains an empty result. On Windows 11
  24H2+ `netsh wlan` returns nothing while Location services are off, so the list
  arrives empty on a machine that IS on Wi-Fi. The handler classifies why
  (no_adapter | wlan_service_off | location_disabled | unknown, from a registry
  read plus adapter and service checks) and recovers the SSID through the ungated
  Get-NetConnectionProfile fallback, tagging it `ssid_source: 'nlm'`. Both extra
  calls only run on the empty path.
- `get-device-identity` returns hostname, model and manufacturer, cached per app
  run.

Cost. The research measured networkInterfaces (~1.1 s), cpu (~1.7 s) and
diskLayout (~2.1 s) — far too slow per measurement — so everything derived from
them is computed once and cached under the default gateway, which recomputes when
the machine changes network. The per-measurement calls are the cheap ones and run
concurrently, keeping the added time inside the 1.5 s budget the plan set. Every
capture fails soft: a locked-down PowerShell policy yields null fields, never a
failed measurement.

`app_build_number` is the short commit, baked by generate-build-mode.js (which
already runs before every Electron build), overridable with
GIGA_METER_BUILD_NUMBER for a pipeline that has its own id, falling back to the
app version outside a git checkout. Neither package.json carries a build counter,
so the commit is the only value that distinguishes two builds of one version. The
same generator now also bakes the speed-test SDK versions so `sdk_version` cannot
drift from the dependency that shipped.

Renderer: `DeviceContextService` wraps the IPC calls, failing soft outside
Electron so the web build and the unit tests keep working; the measurement client
captures the context before the test and the upload service maps it onto the
payload. Queued offline measurements carry the fields automatically — the whole
built payload is what goes into IndexedDB.

Note: the attribute list is the research proposal, still pending Vipul's
confirmation against the tracking spreadsheet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The code comments pointed at plan numbers from a personal knowledge base that
nobody outside its owner can open, so the references were dead weight in a public
repo. Same facts, stated on their own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Bump version in package.json and package-lock.json for both Electron and main app to 2.0.4.
- Introduce `isRegistering` flag in ConfirmschoolPage to prevent duplicate school registrations during the confirmation process.
- Update confirmSchool method to handle registration state and prevent multiple submissions.
- Add new Playwright test for verifying that multiple taps on the confirmation button do not create duplicate registrations.
- Minor adjustments to loading behavior and button states in the UI to improve user experience during registration.
fix: one school registration per confirmation, and bump to 2.0.4
feat: capture network/device context and diagnose empty Wi-Fi reads (plan 0008)
…stamp

The base gained the device/network context capture (#86) and the duplicate
registration fix (#87) while this branch was reverting the ndt7 npm package back
to the vendored copy. Both touch measurement-client.service.ts and
upload.service.ts, so the two sides are combined here rather than one replacing
the other:

- measurement-client.service.ts / upload.service.ts merged cleanly: the vendored
  ndt7 import and the serverTimestamp capture from this branch, the
  DeviceContextService injection, the context capture and the device columns from
  the base.
- measurement-client.service.spec.ts conflicted and is resolved by hand. The
  TestBed block keeps this branch's serverTimestamp coverage; the integration
  block keeps the base's DeviceContextService mock and its two retry tests, now
  spying on the vendored ndt7 object instead of the npm one. The base's metadata
  assertion is dropped: `metadata` no longer exists in testConfig, because the
  vendored ndt7.js hardcodes the client name and version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Victor1Ja and others added 4 commits August 27, 2026 17:04
The target branch refactored this registration callback from .subscribe()
to async/await, so the original one-line change no longer applied. Took the
refactored block wholesale.

On this base the fix is also no longer a rename: 'schoolId' is already
written a few lines above from the route param, which carries the very same
school_id, and StorageService goes through localStorage.setItem, so both
sides store the identical string. What was left was a write to 'school_id'
that nothing in the app reads. Removed it rather than renaming it onto a key
that is already correct.

Verified with ng build.

Assisted-by: Claude Opus 5
Remove the dead school_id storage write
feat: revert to vendored ndt7 and capture M-Lab's server clock
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.

2 participants