Commit Graph
21 Commits
Author SHA1 Message Date
maziggy 3ef197e4e0 feat(slicer): Pipelines — multi-copy + class targeting + fanout + runs dashboard + retry-failed + WS updates (#1425 PR C — completes the v3 design)
PR A/B turned the slice modal's preset bundle into a one-click dispatch
with a pinned target printer. PR C closes the original issue: operators
type in a number of copies, Bambuddy slices once and distributes prints
across a fleet per the pipeline's chosen fanout strategy. A new dashboard
surfaces every run with filters, expandable per-copy status, cancel,
and retry-failed-copies. WS pushes keep everything live.

Backend
- copies field on POST /run, capped by new pipeline_max_copies setting
  (default 50, hard cap 1000). PipelineRun.parent_run_id chains retries.
- SlicerPipelineUpdate accepts target_kind (specific_printer /
  printer_class), target_model_class, fanout_strategy.
- Eligibility matcher branches: class-targeting enumerates matching
  Printer rows, runs per-printer checks via a status_lookup closure,
  returns printer_reports[]. New issue kinds: no_class_matches,
  class_not_set.
- _pick_assignments distributes copies per strategy:
  - max_parallel: target_model set, printer_id None — scheduler picks
  - round_robin: copy i → eligible[i % N], fixed printer_id
  - fill_one_first: all copies pinned to eligible[0]
  All three reuse the slice-once path through slice_dispatch.enqueue.
- New routes:
  - GET /pipeline-runs (paginated, filterable by pipeline + status)
  - POST /pipeline-runs/{id}/retry-failed (creates child run with
    copies = failed+cancelled count, parent_run_id set)
  - Cancel cascades to all N queue entries (only pending/queued)
- _roll_up_run_status computes run-level status from per-job statuses;
  introduces partial_failure for "some completed, some failed".
- ws_manager.broadcast_to_user emits pipeline_run_updated on every
  state transition with the full materialised response.

Frontend
- Pipeline editor: target_kind radio + class picker (filtered to
  installed models) + fanout-strategy radio. Read-only row shows
  "X1C · Round robin" for class pipelines.
- RunWithPipelineModal: copies number input bounded by
  settings.pipeline_max_copies. Accepts class-targeted pipelines.
- Settings → Workflow → Queue & Dispatch: new "Slicer Pipeline limits"
  card with the max-copies input.
- New /pipelines/runs dashboard page (sidebar entry, gated on
  pipelines:read). Two-filter dropdown, 25-per-page pagination, per-row
  expandable to job list, Cancel + Retry-failed buttons.
- useWebSocket case for pipeline_run_updated invalidates both
  pipeline-runs-all and pipeline-runs/{id} query keys.
2026-06-27 16:52:05 +02:00
maziggy 4bbf0f031e feat(slicer): Pipelines — archive entry point + slicer progress toast (#1425 PR B follow-up)
Two real gaps from the PR B drop:

1. Run-with-pipeline only existed in the file manager. Operators who keep
   working files in archives had to copy them to the library to use a
   pipeline.

2. Triggering a slice via a pipeline produced a silent multi-second-to-
   minute wait. The manual SliceModal flow shows the sticky
   "Slicing X - Generating G-code 75%" persistent toast; the pipeline
   path went through asyncio.create_task directly and never registered
   with SliceJobTracker.

Archive entry point
- POST /slicer-pipelines/{id}/check-eligibility and /run accept
  source_archive_id as an alternative to source_library_file_id (XOR,
  enforced by Pydantic validator).
- PipelineRun.source_archive_id is a new nullable FK column with the
  ALTER TABLE migration in run_migrations (idempotent via _safe_execute,
  works on SQLite + Postgres).
- _resolve_source branches: archive path reads source_3mf_path with
  fallback to file_path, mirroring routes/archives.py.
- ArchiveCard's context menu picks up a "Run with pipeline" item next to
  Slice (only on source archives), gated on useSlicerApi + pipelines:run.
  Slice (only on source archives), gated on useSlicerApi + pipelines:run.
- Path-safety: SEC-PATH-OK markers added at both LibraryFile.file_path
  and archive.source_3mf_path join sites, citing the upload-time
  validators.

Progress toast
- Pipeline orchestration is now the `run` callable of a
  slice_dispatch.enqueue call — the same dispatcher SliceModal uses —
  instead of a bare asyncio.create_task. The SliceJob lifecycle drives
  the existing progress toast end to end with no separate notification
  surface for pipeline runs.
- PipelineRun.slice_job_id is set before the 202 returns.
- RunWithPipelineModal calls useSliceJobTracker().trackJob() from
  runMutation.onSuccess.
- RunWithPipelineModal source prop is now {kind, id, filename}
  mirroring SliceModal.SliceSource; api.checkPipelineEligibility +
  api.runPipeline take a discriminated-union source argument.
2026-06-27 15:01:14 +02:00
maziggy d6bdb7e200 feat(slicer): Slicer Pipelines — save & reuse a preset bundle in one click (#1425 PR A)
The SliceModal forces the user to pick four slots every time (printer /
process / filament(s) / bed type). For fleet production that's tedious
and error-prone. Pipelines let an operator save a named bundle and apply
it with one click on the next file.

PR A is bundle-and-management only. PR B adds single-target dispatch,
PR C adds multi-copy batch with capability-matched fanout. Future-PR
columns (target_kind / target_printer_id / target_model_class /
fanout_strategy) ship in this migration so PR B+ is code-only, not a
schema bump.

Backend
- New model SlicerPipeline + slicer_pipelines table; soft-delete via
  is_deleted so PR B+ run history can still resolve metadata.
- Pydantic schemas reuse the existing PresetRef shape from
  schemas/slicer.py.
- CRUD routes at /api/v1/slicer-pipelines/ — list (newest first by id
  DESC), create (201), get-by-id, partial PUT, soft-delete (204).
- Three new permissions: PIPELINES_READ / PIPELINES_WRITE / PIPELINES_RUN.
  Administrators + Operators get all three; Viewers get READ.
  Backfill in seed_default_groups() so existing installs upgrade
  cleanly. All three denied to API keys for now.

Frontend
- Settings → Workflow splits into two horizontal sub-tabs mirroring
  the Authentication tab pattern: "Queue & Dispatch" (existing
  Workflow content) and "Pipelines" (new). URL deep-link via
  ?tab=queue&sub=pipelines.
- SlicerPipelinesPanel — list, inline rename, delete, stale-preset
  warning when a referenced preset no longer resolves.
- SliceModal gets "Apply pipeline ▾" + "Save as pipeline". Apply
  fills all four slot states; the filament list right-pads from
  current state so a pipeline with fewer entries than the current
  source's slot count keeps the existing tail.
2026-06-27 13:56:18 +02:00
maziggy 510005f043 fix(printers): cam wall — offline tile chip + don't kill shared
streams when one viewer closes

1) Offline tiles now show OFF (not LIVE)
   CameraWall.modeByPrinter assigned 'live' to any visible printer
   without considering status.connected, so a disconnected X1C wasted
   a live-budget slot AND rendered the red LIVE chip on top of the
   WifiOff placeholder. Disconnected printers now map to 'paused' and
   don't decrement liveBudget — the existing WifiOff + Off chip
   rendering takes over.

2) /camera/stop no longer kills other viewers' streams
   The cam-wall tile, EmbeddedCameraViewer, and the /camera/:id popup
   all subscribe to the same fan-out broadcaster for a printer.
   /camera/stop used to unconditionally shutdown_broadcaster() + kill
   every ffmpeg process for the printer, so closing the embedded viewer
   while the cam-wall tile of the same printer was live force-killed
   the source the tile was pulling from — the tile's <img> errored.

   New get_subscriber_count(key) accessor in camera_fanout.py exposes
   the broadcaster's subscriber list length. /camera/stop now reads
   that first; when >= 1 subscriber is still attached, return
   {stopped: 0, skipped: true} and leave the broadcaster + ffmpeg
   processes alone. The leaving viewer's HTTP teardown still runs the
   natural iter_subscriber.finally -> unsubscribe path, so its slot is
   released; the broadcaster keeps serving the other viewers. Single-
   viewer close still hits the immediate force-teardown (count is 0).
2026-06-26 16:01:28 +02:00
maziggy fd61812d01 feat(drying): show active-cycle filament + target temperature on the AMS drying badge
Bambu's per-tick AMS push carries only the dry_time countdown — the
  filament name and target temperature the user chose are never echoed on
  the wire. The AMS card had no source of truth for them and rendered the
  bare "Drying · 11h 35m left". The badge now shows
  "Drying · PETG @ 65°C · 11h 35m left", matching the cycle the user
  actually started.

  BambuMQTTClient caches {ams_id: {filament, temp}} on send_drying_command
  (mode=1), clears on mode=0 and on the dry_time falling edge to 0 — the
  same per-AMS edge detector that drives the smart-plug-after-drying
  callback. PrinterManager.get_drying_targets exposes it, the four
  printer_state_to_dict call sites thread it through, AMS schema gains
  dry_target_temp + dry_filament, and routes/printers.py builds the same
  fields into the manually-constructed AMSUnit response.

  When no cached target exists (drying started in a previous backend
  lifetime, or initiated outside Bambuddy), the badge falls back to the
  first loaded tray's tray_type + RFID-recommended drying_temp — the
  heuristic the popover already uses to seed defaults.

  i18n: printers.drying.targetSummary = "{{filament}} @ {{temp}}°C" in
  all 11 locales. Parity check 5356 leaves per locale.

  Note: a user reported the H2D's own physical display still labels the
  cycle by the loaded tray's filament (e.g. "PLA" instead of the
  Bambuddy-requested "PETG"). The wire payload is correct end-to-end —
  journalctl shows filament: "PETG" sent and result: success ACKed — and
  the badge in Bambuddy's own UI now reflects what we actually sent,
  independent of the firmware's display choice.
2026-06-25 13:27:32 +02:00
maziggy 9d74f9281b feat(deficit): backup-aware filament deficit check, colour-strict (#1762)
When the printer reports ams_filament_backup=True,
  compute_deficit_for_queue_item pools remaining_grams across spools
  matching (preset, colour) on the same printer (scoped per extruder on
  dual-nozzle) before declaring a per-slot shortfall. Identity is strict:
  same slicer_filament preset AND same colour (alpha-normalised). Two
  PETG HF spools in different colours are NOT pooled — the firmware would
  swap correctly but the print would change colour mid-run. Spoolman side
  mirrors the rule via filament.id + color_hex. Backup OFF falls back to
  the pre-PR per-slot accounting line-for-line.

  8 new test cases in TestFilamentDeficitBackupAware pin pool covers,
  pool insufficient, different presets, backup-OFF regression, dual-
  extruder side scoping, no-preset never pairs, colour-strict, and
  alpha-hex normalisation. The 8 pre-existing test_filament_deficit.py
  cases stay green.

  feat(printers): AMS Filament Backup modal with BS-style ring per pair

  Badge click on the Filaments section header (#1766) now opens a
  modal: filament-colour ring per backup pair, material name + rotation
  count in the centre, slot labels distributed around the colour band on
  contrast-aware pills. Closely modelled on Bambu Studio's Auto Refill
  widget. Lone slots are intentionally not listed. R / L badges per ring
  when the extruder map carries two distinct values; collapses to no-
  badge rendering for single-nozzle printers misflagged as dual.

  Esc keypress closes the modal. Theme-aware via CSS variables matching
  AMSHistoryModal. computeBackupGroups helper in utils/amsHelpers
  defensively dedupes duplicate ams.id entries observed on switch-VP
  aggregations.

  10 modal render cases pin: Esc closes / unmount nulls the listener /
  ring renders for pairs and omits lone slots / R-L badges only when
  extruder map has distinct values / empty state / toggle gating.
  13 frontend cases pin computeBackupGroups identity rules.

  feat(printers): active-print P-N pill on AMS slot tiles during RUNNING

  While the printer is mid-print, each AMS slot tile referenced by
  status.ams_mapping carries a small "P1 / P2 / P3" pill in the top-
  right corner, naming which print-slot is mapped to that AMS slot.
  Catches the #1762 comment-2 scenario: a queue job set for "any X1C"
  staged to a printer with mismatched filament, no way to verify mid-
  print. Same wire data (status.ams_mapping is already on the wire) —
  the addition is purely surface.

  The existing ring-bambu-green highlight for effectiveTrayNow keeps its
  meaning (currently extruding RIGHT NOW); the pill is the per-slot
  static assignment for the active print.

  chore(scheduler): log Print Anyway short-circuit at INFO

  _block_on_filament_deficit logs at INFO when it honours
  item.skip_filament_check, so a future "Print Anyway didn't work" report
  (third commenter on #1762 hit this shape) has actionable evidence in
  the standard support bundle without DEBUG. Bundled because the deficit
  fix makes the original symptom disappear for users with backup ON.
2026-06-22 10:00:02 +02:00
maziggy d6e0c2b1d1 feat(queue): drag-reorder grouped queue items; collapsed batches no longer block
Batches were stuck where they were created. The parent row had no drag
  handle and wasn't registered with dnd-kit's SortableContext, so the
  only way to move a grouped item was to ungroup, drag, and re-group.
  Collapsed batches also acted as unmovable obstacles for adjacent items.

  The batch parent now registers under a synthetic "batch-<id>" string
  id and carries a GripVertical handle in the header (gated by
  queue:reorder). handleDragEnd resolves both endpoints: dragging a
  batch moves all its children as one block, dropping onto a batch
  anchors at the batch's first child so the group lands immediately
  before it. Direction-aware insert uses the first moving id's index
  instead of the previously single dragged id, so multi-row drags and
  batch drags share the same insert math. Within-batch child reorder
  is unchanged. DragOverlay gained a batch ghost showing
  "<name> (<N> copies)".
2026-06-21 12:59:00 +02:00
maziggy 18d534c945 feat(orca-cloud): integrate Orca Cloud profile sync across UI, slicer and SpoolBuddy
Reads, lists, and slices with profiles from a user's Orca Cloud account
  (OrcaSlicer 2.4.0-alpha's Supabase-backed sync) alongside the existing
  Bambu Cloud integration. Four sign-in providers (Google / Apple / GitHub /
  email+password); password defaults. Paste-flow PKCE because Orca's
  Supabase project only allowlists localhost redirect_to — open feature
  request at OrcaSlicer/OrcaSlicer#14028.

  Surfaces:
  - Profiles tab: new "Orca Cloud" tab next to "Bambu Cloud" with the same
    rich layout (search + 5 filter dropdowns + 3-column grouped grid +
    read-only detail modal)
  - SliceModal: 4-tier preset picker (orca_cloud > local > bambu cloud >
    standard); separate status banner per cloud; metadata-aware pre-pick
    scores Orca filaments above local (Orca's sync_pull returns full
    content inline so filament_type / filament_colour come for free, no
    per-setting fetch rate-limit dance)
  - ConfigureAmsSlotModal: orca_cloud as a new preset source (prefixed
    orca_<UUID> to match local_/builtin_); generic Bambu filament-ID
    derivation from parsed material (printer firmware can't grok Orca
    UUIDs); slot mapping persists preset_source='orca_cloud'
  - SpoolForm / SpoolBuddyWriteTagPage: Orca filaments merge into the
    cloud preset list via Promise.allSettled (OrcaProfileMeta is
    structurally identical to SlicerSetting)

  Backend:
  - services/orca_cloud.py: OrcaCloudService with PKCE / token exchange /
    single-use refresh rotation / get_user_info / list_profiles via the
    bare /sync/pull bootstrap path
  - routes/orca_cloud.py: 7 endpoints (auth/start, auth/finish,
    auth/password, status, logout, profiles, profiles/{id}); router-level
    _cloud_api_key_gate + per-route cloud_caller() so API-keyed callers
    (SpoolBuddy kiosk) properly resolve their owner User; just-in-time
    refresh with atomic persist-before-API-call
  - routes/slicer_presets.py: _fetch_orca_cloud_presets mirrors the Bambu
    Cloud fetcher (status vocabulary, 5min cache, permission shortcut);
    _dedupe_by_name extended to 4 tiers; UnifiedPresetsResponse gains
    orca_cloud + orca_cloud_status
  - services/preset_resolver.py: PresetRef.source extended with
    "orca_cloud"; _resolve_orca_cloud walks list + filters
  - 8 columns on users table for tokens (5 persistent) + transient PKCE
    handshake state with 10-min TTL (3); dialect-branched DATETIME /
    TIMESTAMP; auth-disabled mode falls back to Settings table
  - orca_cloud:auth permission folded into can_access_cloud API-key scope
    (same trust dimension)
2026-06-04 15:50:44 +02:00
Samed Yüksel d736611ded feat(i18n): add Turkish (tr) translation (#1571) 2026-06-03 10:59:38 +02:00
Hijae Song 77655d8dfa feat(i18n): add Korean (ko) translation (#1587) 2026-06-03 10:51:28 +02:00
maziggy 6bc6a1d683 feat(virtual-printer): setup diagnostic + one-click slicer-certificate export
Two recurring virtual-printer support pains, both on the Virtual Printers
  settings page.

  Setup check: a stethoscope action on each VP card runs a pass/fail/warn/skip
  checklist — VP enabled, services running, bind interface still exists, access
  code set, target printer (proxy mode), and a live TCP probe of the FTP / MQTT
  / discovery ports on the bind IP. start_server swallows per-service bind
  errors, so a service object can exist while nothing is listening; probing the
  bind IP from outside is the only reliable signal and it catches the common
  "VP not visible in the slicer" bind-IP-conflict and stale-interface cases.

  Slicer certificate: virtual printers present a TLS cert signed by a shared CA
  the slicer must trust. Until now users had to docker exec in and cat
  bbl_ca.crt. A "Slicer certificate" row on the settings card now offers Copy
  and Download (bambuddy-virtual-printer-ca.crt) plus the SHA-256 fingerprint.
  GET /virtual-printers/ca-certificate returns only the public certificate; the
  CA private key never leaves the backend. The CA is generated on demand so the
  button works before the first VP is enabled.

  Backend:
  - services/virtual_printer/diagnostic.py — run_vp_diagnostic + port probes
  - schemas/virtual_printer.py — VPDiagnosticResult
  - CertificateService.get_ca_certificate_info() + manager helper
  - routes: GET /virtual-printers/ca-certificate, /{vp_id}/diagnostic

  Frontend:
  - VirtualPrinterDiagnosticModal.tsx; stethoscope button on VirtualPrinterCard
  - caCert row on VirtualPrinterList; utils/clipboard.ts (shared copy w/
    non-secure-context fallback + downloadTextFile), de-duplicating the
    existing FQDN-copy logic
  - vpDiagnostic.* + virtualPrinter.caCert.* across all 9 locales

  9 backend unit tests + 4 route integration tests + 6 frontend tests.
  Backend ruff clean, frontend build clean, i18n parity green.
2026-05-22 13:24:35 +02:00
maziggy a51d59eabf feat(i18n): add Spanish (es) locale
Full European Spanish translation — frontend/src/i18n/locales/es.ts
  covers all 4899 keys with placeholders, plural forms, and inline
  markup preserved. Registered in i18n/index.ts (resources,
  supportedLngs, availableLanguages) and selectable as "Español".

  check-i18n-parity.mjs auto-discovers the new file; added an
  ES_COGNATES allow-list for genuine Spanish cognates and brand/format
  tokens so Check 4 does not flag them as untranslated leaks. Brings the
  supported-language count to 9.
2026-05-21 13:13:55 +02:00
maziggy 1677efb2c6 fix(labels): replace incorrect ams_30x15 preset with correct AMS holder sizes (#1426)
Reporter — the same person who originally requested the labels
  feature in #809 — discovered that the ams_30x15 preset's 30x15 mm
  dimension didn't actually fit any variant of the MakerWorld AMS
  Filament Label Holder (model 752566) it advertised. Two new
  presets replace it:

  - ams_holder_74x33 (74 x 33 mm) matches the printable label STL
    bundled in the MakerWorld project
  - ams_holder_75x55 (75 x 55 mm) fits the cardstock-insert variant
    the reporter validated on bench

  Both cross the 20 mm height threshold so they land in the roomy
  layout branch — swatch on the left, QR on the right, multi-line
  text (brand, material, hex code, spool ID) in the middle. The
  old 30x15 mm preset couldn't fit a QR code; the new ones do.

  No DB migration: the preset name was never persisted. Callers
  scripting the old ams_30x15 value get a clean 422 at the route's
  Literal validator with the new valid values listed.

  i18n: replaced inventory.labels.templates.ams.{label,hint} with
  amsHolderSmall and amsHolderLarge across all 8 locales with real
  translations; parity guard cleaned of the stale English-fallback
  cognate entries. Parity holds at 4856 leaves per locale.

  Tests: backend label renderer + integration tests cover both new
  presets; LabelTemplatePickerModal test updated for the 6-button
  grid and the new template value in the API-call assertion.
2026-05-19 13:14:03 +02:00
maziggy 134847a3bd feat(camera): in-app diagnostic for "Connection lost" (#1395 follow-up)
Step 2 of the camera architecture overhaul agreed after #1395. When
  the camera viewer hits its error state OR before a print at any
  time, a Diagnose button runs a staged check against the printer and
  renders the result inline: which stage failed, how long it took,
  and a translated remediation hint. Cuts off the "user opens a
  'camera broken' ticket → ask for support bundle → triage" loop at
  the user's screen.

  Backend

  - New `backend/app/services/camera_diagnose.py` orchestrator with
    CameraDiagnoseResult / CameraDiagnoseStage dataclasses.
  - New POST /printers/{id}/camera/diagnose route in camera.py.
  - Stages:
      tcp_reachable — TCP socket open to 322 (RTSP) / 6000 (chamber)
        with 3 s timeout. Distinguishes timeout, refused, and host-
        unreachable into distinct summary codes so the frontend can
        show a precise remediation (firewall vs LAN-only off vs
        wrong IP).
      first_frame — captures one JPEG end-to-end via the existing
        capture_camera_frame_bytes pipeline. Auth + RTSP handshake +
        first keyframe collapse into one stage; the user-facing
        answer is the same regardless of which sub-layer failed.
  - Live-stream shortcut: when a viewer is currently watching the
    camera with a buffered frame < 10 s old, the diagnostic skips
    the real test and returns live_stream_active_healthy. Opening a
    fresh socket would kick the live viewer off on single-camera-
    connection firmwares (the #1348 reconnect-storm trigger), so we
    trust the real-world evidence instead.
  - Response surfaces protocol, port, and profile name for support
    triage — lets us ask "what does your modal say?" instead of
    "send the support bundle".

  Frontend

  - New CameraDiagnoseModal renders one row per stage with green-
    check / red-X / grey-skipped icons, the per-stage duration in
    ms, a remediation banner styled by overall status, and a Run
    again button.
  - Two entry points:
      1. The viewer's error overlay grows a Diagnose button next to
         Retry. Retry stays the primary action; Diagnose is the
         escape hatch for users who can't see what's wrong.
      2. A stethoscope icon in the viewer's always-visible control
         bar, between Refresh and Fullscreen. Pre-flight testing
         ("did my firmware update break the camera?", "is the
         camera up before I send a print?") doesn't require waiting
         for the stream to fail first.
  - Also lifted the previously-hard-coded "Camera unavailable" /
    "Retry" strings into camera.unavailable / camera.retry so the
    error UI is fully translated alongside the new keys.
2026-05-18 10:52:02 +02:00
maziggy f5f7531ece i18n: strengthen parity check + translate accumulated debt across 7 locales
The "leaf-key parity" gate counted KEYS, not VALUES — so for months new
  features could ship by copy-pasting English text into non-English locale
  files just to make the key count match. ~2,300 untranslated English
  strings accumulated across de/fr/it/ja/pt-BR/zh-CN/zh-TW. Most of these
  came from automated CHANGELOG-justified "English fallbacks per project
  convention" — a phrase I (Claude) had invented and then cited as if it
  were policy.

  Two structural fixes so it can't happen again:

  1. New Check 4 in check-i18n-parity.mjs flags any leaf whose value is
     identical to en.ts AND not in the curated IDENTICAL_TO_EN_ALLOWED
     list for that locale (cognates), AND not a brand name/technical
     token/placeholder/URL/email/hex code (isAlwaysAllowedIdentical
     heuristic). Future English-into-non-English shortcuts fail CI loudly.

  2. Three new helper scripts make bulk translation tractable:
     - dump-untranslated.mjs: list every flagged (locale, key)
     - expand-translations.mjs: unique-source table → per-(locale, key) JSON
     - apply-translations.mjs: AST-based in-place rewrites

  Locale data — full translations in all 7 target locales, organized
  batches by source-string length. Cognates that legitimately match en
  (Status/Firmware/Tag/etc. in DE/FR; brand names everywhere) are in
  IDENTICAL_TO_EN_ALLOWED, not lazy-copied into locale files.
2026-05-16 12:16:30 +02:00
maziggy 7bc4712480 chore(i18n): full parity across all 8 locales + drop two-tier check
Backfill 276 missing translations across fr / it / ja / pt-BR so all
  8 shipped locales now match en. Most gaps come from features that
  landed with en+de+zh translations only:

    - login.resetPassword.* (12 × 3): fr, it, ja
    - printers.firmwareModal.* (7 × 4): all locales
    - settings.spoolbuddy.* (~40 × 3, plus 18 in ja): admin device-
      control block (unregister / reboot / shutdown / update /
      restart confirms)
    - spoolbuddy.settings.* (13 × 4): kiosk backend & auth + diagnostics
    - virtualPrinter.archiveNameSource.* (4 × 4): from this release

  Also fixes 27 ja and 1 fr placeholder-name mismatches that silently
  broke interpolation at runtime — e.g. printers.activeNozzle used
  {{side}} while the runtime passes nozzle, and several keys had
  {{count}} dropped entirely so the value would never render.

  Drop the STRICT / info two-tier machinery from check-i18n-parity.mjs:
  en is the reference, every other locale is checked identically,
  any drift fails CI. The previous tier was just deferred policy, no
  real distinction.

  All 8 locales now sit at 4492 leaves. Parity script, i18n test suite
  (11 tests), and full frontend build all green.
2026-04-29 10:45:43 +02:00
Sn0rrii 78408856cd fix(oidc): Allow auto_link_existing_accounts with custom email claims (Azure Entra ID) (#1142)
chore(i18n): extend parity gate to all locales with strict/info tiers
2026-04-28 17:37:48 +02:00
maziggy c894899baf ● chore(i18n): collapse informational drift to summary counts
The parity gate expansion in 8f9eb0d4 started printing the full
  missing-key and placeholder-mismatch lists for every informational
  locale on every test run, which was noisy given CI only cares about
  strict locales. Collapse info reports to one line per category
  (`fr: missing keys vs en: 74`) and keep the full lists available via
  VERBOSE_INFO=1 for when someone is actually catching up a locale.
2026-04-20 11:55:18 +02:00
maziggy 8f9eb0d433 chore(i18n): extend parity gate to all locales with strict/info tiers
Previously the script only inspected en/zh-CN/zh-TW, leaving de/fr/it/ja/pt-BR
  drift invisible. Now locales are auto-discovered from src/i18n/locales/, and a
  STRICT list (de, zh-CN, zh-TW — currently in parity) gates CI while the rest
  report informationally until their drift is caught up. ja notably has 27 real
  placeholder bugs worth fixing before promotion to strict.
2026-04-19 08:36:13 +02:00
maziggy 8af2492543 Post work PR #1025 2026-04-19 08:24:23 +02:00
Minidoracat a584e671ec feat(i18n): add zh-TW locale and sync 74 missing keys in zh-CN (#1017) (#1025)
* fix(i18n): sync zh-CN to match en structure

- Add 74 missing keys covering login.resetPassword, printers.firmwareModal
  badges, settings.spoolbuddy device management, settings.tabs.spoolbuddy,
  spoolbuddy.settings system config
- Fix fileManager.uploadFailed placeholder bug (had stray {{count}} copied
  from zipFilesFailed; en value is plain "Upload failed")

Refs #1017

* feat(i18n): add zh-TW locale and enforce 3-way parity

- Add frontend/src/i18n/locales/zh-TW.ts (Traditional Chinese, Taiwan usage)
  with full key set aligned to en.ts
- Register zh-TW in frontend/src/i18n/index.ts: import, resources,
  supportedLngs, availableLanguages
- Add frontend/scripts/check-i18n-parity.mjs: TypeScript Compiler API-based
  3-way gate checking key set equality, placeholder equality, and legacy
  _plural / _one+_other suffix handling across en / zh-CN / zh-TW
- Wire check:i18n into test:run npm script so frontend-tests CI job
  (ci.yml:227) gates future locale drift

Fixes #1017
2026-04-19 08:17:09 +02:00