refactor(inventory): rename /reset-usage to /reset-consumed-counter to match what it actually does (issue #1644)

The old endpoint name implied that calling it would drop weight_used to
  0. In practice it only stamps weight_used_baseline = weight_used so the
  Inventory page's "Total Consumed" widget (weight_used - baseline) reads
  0 going forward, while remaining (label_weight - weight_used) is
  preserved. Calling the endpoint via curl and seeing weight_used
  unchanged in the JSON response is confusing.

  New paths:
  - internal: /api/v1/inventory/spools/{id}/reset-consumed-counter
             /api/v1/inventory/spools/reset-consumed-counter-bulk
  - spoolman: /api/v1/spoolman/inventory/spools/{id}/reset-consumed-counter
             /api/v1/spoolman/inventory/spools/reset-consumed-counter-bulk

  Behaviour is unchanged in both modes; internal stamps the baseline
  directly, Spoolman-mode PATCHes upstream used_weight=0 and the
  _map_spoolman_spool read mapping reconstructs the same "displayed
  consumed = 0, remaining unchanged" Bambuddy-visible shape. Parity
  between modes was already in place and is preserved.

  The Spoolman-client method reset_spool_usage keeps its name because it
  describes what is sent upstream to Spoolman, not what Bambuddy's
  endpoint promises to callers.

  Frontend:
  - api.resetSpoolUsage / bulkResetSpoolUsage (and Spoolman variants)
    renamed to resetSpoolConsumedCounter / bulkResetSpoolConsumedCounter.
  - Button labels: "Reset usage to 0" -> "Reset counter" / "Reset all
    counters" (short, unambiguous); tooltips and confirm-modal bodies
    still spell out the full semantics.
This commit is contained in:
maziggy
2026-06-05 09:22:05 +02:00
parent e802bfc806
commit 9554ebd05d
21 changed files with 212 additions and 184 deletions
+3
View File
@@ -13,6 +13,9 @@ All notable changes to Bambuddy will be documented in this file.
### Added
- **VP MQTT bridge surfaces why `net.info[].ip` rewrite didn't arm (#1429 defensive)** — `MQTTBridge._refresh_ip_encoding` had 4 silent early-return paths (`target_client is None`, `printer client has no ip_address yet`, `no host interface shares a subnet with printer IP X and bind_address is 0.0.0.0/empty`, `invalid IPv4 …`). When the rewrite silently no-op'd on a user's setup, the only signal was the absence of the `MQTT bridge IP encoding armed` INFO line — diagnosing which path was firing meant grepping the source. Each path now emits one `MQTT bridge IP encoding NOT armed: <specific reason>` INFO line; the message names the actual failure (target IP, the missing-interface case, etc.). Throttled via a `_not_armed_reason` dedup field so an idle unarmed bridge doesn't spam one line per 30s refresh tick — only state changes log. Cleared on successful arm so a regression (e.g. printer client unbinds) re-emits the diagnostic. 5 new tests in `TestNotArmedDiagnosticLogging` pin each path's specific reason text, the once-per-state-change throttle, and the arm-clears-dedup behaviour. **Not a fix for #1429 itself** — the bridge logic is unchanged; this just turns the silent failure into visible signal so the next "fix didn't work for me" report can be triaged in one round-trip instead of multiple.
### Changed
- **Inventory: `/reset-usage` renamed to `/reset-consumed-counter`; UI label is now "Reset counter"** — The old endpoint name implied that calling it would drop `weight_used` to 0; in practice it only stamps `weight_used_baseline = weight_used` so the Inventory page's "Total Consumed" widget (which renders `weight_used - baseline`) reads 0 going forward, while remaining (`label_weight - weight_used`) is preserved. Calling the endpoint via curl and seeing `weight_used` unchanged in the JSON response is confusing — the name didn't describe what the endpoint actually does. New paths: internal `/api/v1/inventory/spools/{id}/reset-consumed-counter` + `/spools/reset-consumed-counter-bulk`, Spoolman-mode `/api/v1/spoolman/inventory/spools/{id}/reset-consumed-counter` + `/spools/reset-consumed-counter-bulk`. **Behaviour is unchanged in both modes**: internal stamps the baseline directly; Spoolman-mode PATCHes upstream `used_weight=0` and the `_map_spoolman_spool` read mapping at `_spoolman_helpers.py:252-268` reconstructs the same `displayed consumed = 0, remaining unchanged` Bambuddy-visible shape — Bambuddy-side parity between modes (per [[feedback_inventory_modes_parity]]) was already in place before this rename and is preserved. The Spoolman-client method `reset_spool_usage` keeps its name because it describes what's sent upstream to Spoolman, which has not been renamed. **Frontend**: `api.resetSpoolUsage` / `bulkResetSpoolUsage` (and Spoolman variants) renamed to `resetSpoolConsumedCounter` / `bulkResetSpoolConsumedCounter`. Button labels switch from "Reset usage to 0" to "Reset counter" / "Reset all counters" — short and unambiguous; tooltips and confirm-modal bodies still spell out the full semantics ("zero the consumed-grams counter; remaining weight is not changed"). **i18n**: 9 keys renamed (`resetUsage*`, `resetAllUsage*`, `usageReset`, `allUsageReset`, `resetUsageFailed` → `resetConsumedCounter*`, `resetAllConsumedCounters*`, `consumedCounterReset`, `allConsumedCountersReset`, `resetConsumedCounterFailed`); real translations shipped in all 10 non-English locales (de / es / fr / it / ja / ko / pt-BR / tr / zh-CN / zh-TW) per the hard-rule against English fallbacks. **Tests**: `test_spool_reset_usage.py` (9 tests, internal mode) and 3 reset-related tests in `test_spoolman_inventory_api.py` updated to hit the new paths; behavioural assertions unchanged. **Breaking change for external API consumers** that already wired `/reset-usage` — no compat shim shipped because the old name actively misled callers; the migration is a one-line URL swap.
### Fixed
- **X2D archives lose 3MF metadata because FTPS handshake fails on firmware 01.01.00.00 (#1638, reported by @vasmarfas)** — Reporter's first archive entries from a brand-new X2D landed almost empty (only print time visible, no filament weight / layers / MakerWorld link / thumbnail), and Spoolman filament-usage tracking also went silent. The support bundle traces the symptom end-to-end: at print start `backend/app/main.py::on_print_start` tries the usual FTP-download dance for the 3MF, every connect attempt to the printer fails with `[SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1032)`, and ~2 minutes later `Could not find 3MF file for print: /data/Metadata/plate_1.gcode` → `Created fallback archive N for <name> (no 3MF available)`. The fallback path writes the row with `file_path=""`, `file_size=0`, `content_hash=NULL`, and no layers / filament / model-link fields — exactly the "almost empty card" in the reporter's screenshot. Spoolman tracking and reprint-grouping also degrade from the same root cause: both depend on metadata pulled out of the 3MF by `ThreeMFParser`. The proximate cause is the FTPS handshake: Python 3.13's default `ssl.create_default_context()` negotiates TLS 1.3, and the X2D's implicit-FTPS server on port 990 rejects the ClientHello with `WRONG_VERSION_NUMBER`. This is the same shape of symptom as the P2S 01.02.00.00 FTPS bug from #1401 — handshake / data-channel breakage triggered by the move to Python 3.13's TLS-1.3 default — but the wire-level failure mode is different (P2S completes the handshake and truncates mid-stream with 426; X2D fails the handshake outright). Both are addressed via the per-model registry that #1401 established: `backend/app/services/ftp_profiles.py` gains an `X2D` entry with `cap_tls_v1_2=True` plus a `N6 → X2D` SSDP alias, so the X2D's `ImplicitFTP_TLS` connection caps the SSL context's `maximum_version` to TLS 1.2 and the ClientHello looks like the one the firmware accepted before the Python upgrade. Deliberately conservative — every other model stays on negotiated TLS 1.3, only X2D-tagged sessions flip. **Honest caveat**: this ships as a hypothesis-driven trial rather than a confirmed root-cause fix. The TLS-1.2 cap is the most likely cure given the symptom's family resemblance to #1401, but `WRONG_VERSION_NUMBER` could equally describe the X2D switching to explicit FTPS (AUTH TLS on a plaintext greeting) or moving the FTPS service to a different port — both would need a different code path. The reporter has been asked to test this build; if the cap doesn't clear the error, the registry slot stays useful as a tuning anchor and the next round of diagnostics (`openssl s_client -connect <ip>:990 -tls1_2` from a network-adjacent host) will tell us which of (2)/(3) applies. **Tests**: 3 new in `test_ftp_profiles.py` mirroring the existing P2S coverage — `X2D` resolves to `cap_tls_v1_2=True`, `N6` SSDP code aliases to the X2D profile, lowercase `x2d` still hits the cap. Existing P2S + default + unknown-model + frozen-dataclass + non-capped-spot-check (X1C / H2D / P1S / A1) tests stay green. **Verified**: ruff clean; the integration test at `test_cap_tls_v1_2_actually_applied_to_ssl_context` already pins the profile→`ImplicitFTP_TLS`→`ssl_context.maximum_version` wiring so this entry can't silently fail to apply.
+10 -4
View File
@@ -1071,8 +1071,8 @@ async def restore_spool(
return result.scalar_one()
@router.post("/spools/{spool_id}/reset-usage", response_model=SpoolResponse)
async def reset_spool_usage(
@router.post("/spools/{spool_id}/reset-consumed-counter", response_model=SpoolResponse)
async def reset_spool_consumed_counter(
spool_id: int,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
@@ -1084,6 +1084,12 @@ async def reset_spool_usage(
weight_used` (remaining) is unchanged. weight_locked is also left
alone — the spool keeps receiving AMS auto-sync updates. Matches
Spoolman's split between used_weight and remaining_weight (#1390).
The earlier name `/reset-usage` was misleading: callers reasonably
expected `weight_used` itself to drop to 0 and were surprised when
the response showed it unchanged. The current name describes what
the endpoint actually does — reset the "Total Consumed" counter
widget, not the lifetime weight_used field.
"""
result = await db.execute(select(Spool).where(Spool.id == spool_id))
spool = result.scalar_one_or_none()
@@ -1097,8 +1103,8 @@ async def reset_spool_usage(
return result.scalar_one()
@router.post("/spools/reset-usage-bulk")
async def bulk_reset_spool_usage(
@router.post("/spools/reset-consumed-counter-bulk")
async def bulk_reset_spool_consumed_counter(
payload: dict,
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
+17 -7
View File
@@ -865,13 +865,23 @@ async def restore_spool(
raise HTTPException(status_code=502, detail="Spoolman returned malformed spool data") from exc
@router.post("/spools/{spool_id}/reset-usage")
async def reset_spool_usage(
@router.post("/spools/{spool_id}/reset-consumed-counter")
async def reset_spool_consumed_counter(
spool_id: int = Path(..., gt=0),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
) -> dict:
"""Zero the spool's used_weight in Spoolman without touching anything else."""
"""Zero the displayed "Total Consumed" counter for a Spoolman spool.
Spoolman doesn't have a native "baseline" field, so the implementation
reaches for the closest equivalent: PATCH `used_weight=0` upstream.
The read mapping in ``_map_spoolman_spool`` then derives Bambuddy's
`weight_used = label - remaining_weight` and `baseline = weight_used -
real_used_weight`, so the Inventory page's `weight_used - baseline`
display lands at 0 while remaining (= label - weight_used) is preserved
— parity with the internal-mode endpoint (#1390, see also
``backend/app/api/routes/inventory.py::reset_spool_consumed_counter``).
"""
client = await _get_client(db)
async with _translate_spoolman_errors():
spool = await client.reset_spool_usage(spool_id)
@@ -882,13 +892,13 @@ async def reset_spool_usage(
raise HTTPException(status_code=502, detail="Spoolman returned malformed spool data") from exc
@router.post("/spools/reset-usage-bulk")
async def bulk_reset_spool_usage(
@router.post("/spools/reset-consumed-counter-bulk")
async def bulk_reset_spool_consumed_counter(
payload: dict = Body(...),
db: AsyncSession = Depends(get_db),
_: User | None = RequirePermissionIfAuthEnabled(Permission.INVENTORY_UPDATE),
) -> dict:
"""Bulk-reset used_weight to 0 across the given Spoolman spool IDs.
"""Bulk reset the "Total Consumed" counter across the given Spoolman spool IDs.
Caller passes an explicit list of IDs — no "reset all" shortcut, since
a typo on a wildcard would wipe the entire inventory's tracking.
@@ -909,7 +919,7 @@ async def bulk_reset_spool_usage(
await client.reset_spool_usage(spool_id)
reset_count += 1
except HTTPException as exc:
logger.warning("Spoolman reset-usage failed for spool %s: %s", spool_id, exc.detail)
logger.warning("Spoolman reset-consumed-counter failed for spool %s: %s", spool_id, exc.detail)
return {"reset": reset_count}
@@ -1,4 +1,9 @@
"""Reset-usage endpoint regressions (#1390 follow-up).
"""Reset-consumed-counter endpoint regressions (#1390 follow-up).
Endpoint paths renamed from ``/reset-usage`` to ``/reset-consumed-counter``
to match what the endpoint actually does (the previous name implied
``weight_used`` itself would drop to 0, which surprised callers reading
the JSON response — see the discussion that drove this rename).
The per-spool and bulk reset endpoints stamp `weight_used_baseline =
weight_used` instead of zeroing `weight_used` directly. This decouples
@@ -60,7 +65,7 @@ class TestResetSpoolUsage:
"""
spool = await spool_factory(label_weight=1000, weight_used=456.0)
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-consumed-counter")
assert response.status_code == 200
body = response.json()
@@ -86,7 +91,7 @@ class TestResetSpoolUsage:
"""
spool = await spool_factory(weight_used=100.0, weight_locked=False)
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-consumed-counter")
assert response.status_code == 200
await db_session.refresh(spool)
@@ -100,7 +105,7 @@ class TestResetSpoolUsage:
"""If the user previously locked the spool, the lock is preserved."""
spool = await spool_factory(weight_used=500.0, weight_locked=True)
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
response = await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-consumed-counter")
assert response.status_code == 200
await db_session.refresh(spool)
@@ -117,7 +122,7 @@ class TestResetSpoolUsage:
counter while remaining keeps decrementing normally.
"""
spool = await spool_factory(label_weight=1000, weight_used=456.0)
await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-usage")
await async_client.post(f"/api/v1/inventory/spools/{spool.id}/reset-consumed-counter")
# Simulate a 50g print (usage_tracker increments weight_used).
await db_session.refresh(spool)
@@ -133,7 +138,7 @@ class TestResetSpoolUsage:
@pytest.mark.asyncio
@pytest.mark.integration
async def test_reset_404_for_missing_spool(self, async_client: AsyncClient):
response = await async_client.post("/api/v1/inventory/spools/99999/reset-usage")
response = await async_client.post("/api/v1/inventory/spools/99999/reset-consumed-counter")
assert response.status_code == 404
@@ -149,7 +154,7 @@ class TestBulkResetSpoolUsage:
untouched = await spool_factory(weight_used=300.0)
response = await async_client.post(
"/api/v1/inventory/spools/reset-usage-bulk",
"/api/v1/inventory/spools/reset-consumed-counter-bulk",
json={"spool_ids": [target1.id, target2.id]},
)
@@ -173,7 +178,7 @@ class TestBulkResetSpoolUsage:
async def test_bulk_reset_rejects_empty_list(self, async_client: AsyncClient):
"""Empty list must be rejected — guards against accidental wildcard wipes."""
response = await async_client.post(
"/api/v1/inventory/spools/reset-usage-bulk",
"/api/v1/inventory/spools/reset-consumed-counter-bulk",
json={"spool_ids": []},
)
assert response.status_code == 400
@@ -183,7 +188,7 @@ class TestBulkResetSpoolUsage:
async def test_bulk_reset_rejects_missing_field(self, async_client: AsyncClient):
"""Missing spool_ids field must be rejected."""
response = await async_client.post(
"/api/v1/inventory/spools/reset-usage-bulk",
"/api/v1/inventory/spools/reset-consumed-counter-bulk",
json={},
)
assert response.status_code == 400
@@ -196,7 +201,7 @@ class TestBulkResetSpoolUsage:
locked = await spool_factory(weight_used=200.0, weight_locked=True)
response = await async_client.post(
"/api/v1/inventory/spools/reset-usage-bulk",
"/api/v1/inventory/spools/reset-consumed-counter-bulk",
json={"spool_ids": [unlocked.id, locked.id]},
)
@@ -560,13 +560,13 @@ class TestSpoolmanInventoryCRUD:
@pytest.mark.asyncio
@pytest.mark.integration
async def test_reset_spool_usage(
async def test_reset_spool_consumed_counter(
self,
async_client: AsyncClient,
spoolman_settings,
mock_spoolman_client,
):
"""POST /spoolman/inventory/spools/{id}/reset-usage zeroes used_weight in Spoolman.
"""POST /spoolman/inventory/spools/{id}/reset-consumed-counter zeroes the displayed counter.
Parity with internal mode (#1390): the InventorySpool response
carries `weight_used = label - remaining` and
@@ -575,7 +575,7 @@ class TestSpoolmanInventoryCRUD:
while remaining (= label - weight_used) preserves Spoolman's
independent remaining_weight field.
"""
response = await async_client.post("/api/v1/spoolman/inventory/spools/42/reset-usage")
response = await async_client.post("/api/v1/spoolman/inventory/spools/42/reset-consumed-counter")
assert response.status_code == 200
body = response.json()
@@ -588,15 +588,15 @@ class TestSpoolmanInventoryCRUD:
@pytest.mark.asyncio
@pytest.mark.integration
async def test_bulk_reset_spool_usage(
async def test_bulk_reset_spool_consumed_counter(
self,
async_client: AsyncClient,
spoolman_settings,
mock_spoolman_client,
):
"""Bulk endpoint resets each listed spool and returns the count."""
"""Bulk endpoint resets each listed spool's counter and returns the count."""
response = await async_client.post(
"/api/v1/spoolman/inventory/spools/reset-usage-bulk",
"/api/v1/spoolman/inventory/spools/reset-consumed-counter-bulk",
json={"spool_ids": [1, 2, 3]},
)
@@ -614,7 +614,7 @@ class TestSpoolmanInventoryCRUD:
):
"""Empty list must be rejected — guards against accidental wildcard wipes."""
response = await async_client.post(
"/api/v1/spoolman/inventory/spools/reset-usage-bulk",
"/api/v1/spoolman/inventory/spools/reset-consumed-counter-bulk",
json={"spool_ids": []},
)
@@ -179,7 +179,7 @@ describe('InventoryPage — Total Consumed includes archived (#1390 follow-up)',
});
});
it('Reset-usage eraser is rendered for archived spools too', async () => {
it('Reset-consumed-counter eraser is rendered for archived spools too', async () => {
// The per-card eraser is gated on weight_used > 0, NOT on archived_at,
// so the archived spool above (weight_used=500) must render an eraser
// button matching the localized tooltip. Multiple erasers exist on the
@@ -191,19 +191,19 @@ describe('InventoryPage — Total Consumed includes archived (#1390 follow-up)',
// surface archived spools first; the easiest assertion that doesn't
// depend on chip clicks is via the bulk-reset wiring: when archived
// are included in the resetable set, the total is non-zero — i.e.
// the "Reset all usage" button stays visible. The CHANGELOG entry
// the "Reset all counters" button stays visible. The CHANGELOG entry
// walks through the per-card flow.
render(<InventoryPageRouter />);
await waitFor(() => {
// Reset-all-usage button is gated on `totalConsumed > 0 &&
// Reset-all-consumed-counters button is gated on `totalConsumed > 0 &&
// resetableSpoolIds.length > 0`. resetableSpoolIds now includes
// archived spools — so even if every active spool had its baseline
// == weight_used (consumed = 0), the button must remain visible
// as long as ANY spool (archived included) still has unreset usage.
// The 800g assertion already proves totalConsumed > 0; here we
// just check the bulk-reset button rendered.
expect(screen.getByRole('button', { name: /reset all spool usage/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /reset all counters/i })).toBeInTheDocument();
});
});
});
+8 -8
View File
@@ -4883,10 +4883,10 @@ export const api = {
request<InventorySpool>(`/inventory/spools/${id}/archive`, { method: 'POST' }),
restoreSpool: (id: number) =>
request<InventorySpool>(`/inventory/spools/${id}/restore`, { method: 'POST' }),
resetSpoolUsage: (id: number) =>
request<InventorySpool>(`/inventory/spools/${id}/reset-usage`, { method: 'POST' }),
bulkResetSpoolUsage: (spoolIds: number[]) =>
request<{ reset: number }>(`/inventory/spools/reset-usage-bulk`, {
resetSpoolConsumedCounter: (id: number) =>
request<InventorySpool>(`/inventory/spools/${id}/reset-consumed-counter`, { method: 'POST' }),
bulkResetSpoolConsumedCounter: (spoolIds: number[]) =>
request<{ reset: number }>(`/inventory/spools/reset-consumed-counter-bulk`, {
method: 'POST',
body: JSON.stringify({ spool_ids: spoolIds }),
}),
@@ -5054,10 +5054,10 @@ export const api = {
request<InventorySpool>(`/spoolman/inventory/spools/${id}/archive`, { method: 'POST' }),
restoreSpoolmanInventorySpool: (id: number) =>
request<InventorySpool>(`/spoolman/inventory/spools/${id}/restore`, { method: 'POST' }),
resetSpoolmanInventorySpoolUsage: (id: number) =>
request<InventorySpool>(`/spoolman/inventory/spools/${id}/reset-usage`, { method: 'POST' }),
bulkResetSpoolmanInventorySpoolUsage: (spoolIds: number[]) =>
request<{ reset: number }>(`/spoolman/inventory/spools/reset-usage-bulk`, {
resetSpoolmanInventorySpoolConsumedCounter: (id: number) =>
request<InventorySpool>(`/spoolman/inventory/spools/${id}/reset-consumed-counter`, { method: 'POST' }),
bulkResetSpoolmanInventorySpoolConsumedCounter: (spoolIds: number[]) =>
request<{ reset: number }>(`/spoolman/inventory/spools/reset-consumed-counter-bulk`, {
method: 'POST',
body: JSON.stringify({ spool_ids: spoolIds }),
}),
+9 -9
View File
@@ -3829,15 +3829,15 @@ export default {
inPrinter: 'Im Drucker',
lowStock: 'Niedriger Bestand',
sinceTracking: 'Seit Beginn der Erfassung',
resetUsage: 'Verbrauch auf 0 zurücksetzen',
resetUsageTooltip: 'Den verbrauchten Gramm-Zähler dieser Spule auf null setzen',
resetUsageConfirm: 'Verbrauchten Gramm-Zähler dieser Spule auf 0 zurücksetzen? Künftige Drucke zählen wieder ab null. Die Spule selbst, ihre Restgewichtsberechnung und Ihre Einstellungen bleiben unverändert.',
resetAllUsage: 'Verbrauch aller Spulen zurücksetzen',
resetAllUsageTooltip: 'Den verbrauchten Gramm-Zähler auf jeder Spule auf null setzen',
resetAllUsageConfirm: 'Verbrauchten Gramm-Zähler auf allen {{count}} Spulen (archivierte eingeschlossen) auf 0 zurücksetzen? Das löscht den Wert „Insgesamt verbraucht“, sodass künftige Drucke wieder ab null gezählt werden. Spulen und Restgewichte bleiben unverändert.',
usageReset: 'Spulenverbrauch auf 0 zurückgesetzt',
allUsageReset: '{{count}} Spule(n) zurückgesetzt',
resetUsageFailed: 'Zurücksetzen des Spulenverbrauchs fehlgeschlagen',
resetConsumedCounter: 'Zähler zurücksetzen',
resetConsumedCounterTooltip: 'Den verbrauchten Gramm-Zähler dieser Spule auf null setzen. Das Restgewicht bleibt unverändert.',
resetConsumedCounterConfirm: 'Verbrauchten Gramm-Zähler dieser Spule auf 0 zurücksetzen? Künftige Drucke zählen wieder ab null. Die Spule selbst, ihre Restgewichtsberechnung und Ihre Einstellungen bleiben unverändert.',
resetAllConsumedCounters: 'Alle Zähler zurücksetzen',
resetAllConsumedCountersTooltip: 'Den verbrauchten Gramm-Zähler auf jeder Spule auf null setzen. Die Restgewichte bleiben unverändert.',
resetAllConsumedCountersConfirm: 'Verbrauchten Gramm-Zähler auf allen {{count}} Spulen (archivierte eingeschlossen) auf 0 zurücksetzen? Das löscht den Wert „Insgesamt verbraucht“, sodass künftige Drucke wieder ab null gezählt werden. Spulen und Restgewichte bleiben unverändert.',
consumedCounterReset: 'Zähler zurückgesetzt',
allConsumedCountersReset: 'Zähler für {{count}} Spule(n) zurückgesetzt',
resetConsumedCounterFailed: 'Zähler konnte nicht zurückgesetzt werden',
loadedInAms: 'Im AMS/Ext geladen',
remaining: 'Verbleibend',
weightCheck: 'Gewichtskontrolle',
+9 -9
View File
@@ -3841,15 +3841,15 @@ export default {
inPrinter: 'In Printer',
lowStock: 'Low Stock',
sinceTracking: 'Since tracking started',
resetUsage: 'Reset usage to 0',
resetUsageTooltip: 'Zero the consumed-grams counter for this spool',
resetUsageConfirm: 'Reset this spool\'s consumed-grams counter to 0? Future prints will track from zero again. The spool itself, its remaining weight calculation, and your settings are not changed.',
resetAllUsage: 'Reset all spool usage',
resetAllUsageTooltip: 'Zero the consumed-grams counter on every spool',
resetAllUsageConfirm: 'Reset the consumed-grams counter to 0 on all {{count}} spools (archived ones included)? This clears the "Total Consumed" stat so future prints track from zero. Spools and remaining weights are not changed.',
usageReset: 'Spool usage reset to 0',
allUsageReset: 'Reset {{count}} spool(s)',
resetUsageFailed: 'Failed to reset spool usage',
resetConsumedCounter: 'Reset counter',
resetConsumedCounterTooltip: 'Zero the consumed-grams counter for this spool. Remaining weight is not changed.',
resetConsumedCounterConfirm: 'Reset this spool\'s consumed-grams counter to 0? Future prints will track from zero again. The spool itself, its remaining weight calculation, and your settings are not changed.',
resetAllConsumedCounters: 'Reset all counters',
resetAllConsumedCountersTooltip: 'Zero the consumed-grams counter on every spool. Remaining weights are not changed.',
resetAllConsumedCountersConfirm: 'Reset the consumed-grams counter to 0 on all {{count}} spools (archived ones included)? This clears the "Total Consumed" stat so future prints track from zero. Spools and remaining weights are not changed.',
consumedCounterReset: 'Counter reset',
allConsumedCountersReset: 'Counter reset for {{count}} spool(s)',
resetConsumedCounterFailed: 'Failed to reset counter',
loadedInAms: 'Loaded in AMS/Ext',
remaining: 'Remaining',
weightCheck: 'Weight Check',
+9 -9
View File
@@ -3837,15 +3837,15 @@ export default {
inPrinter: 'En la impresora',
lowStock: 'Existencias bajas',
sinceTracking: 'Desde que comenzó el seguimiento',
resetUsage: 'Restablecer el uso a 0',
resetUsageTooltip: 'Poner a cero el contador de gramos consumidos de esta bobina',
resetUsageConfirm: '¿Restablecer a 0 el contador de gramos consumidos de esta bobina? Las impresiones futuras volverán a contar desde cero. La bobina en sí, su cálculo de peso restante y sus ajustes no cambian.',
resetAllUsage: 'Restablecer el uso de todas las bobinas',
resetAllUsageTooltip: 'Poner a cero el contador de gramos consumidos de todas las bobinas',
resetAllUsageConfirm: '¿Restablecer a 0 el contador de gramos consumidos de las {{count}} bobinas (incluidas las archivadas)? Esto borra la estadística «Total consumido» para que las impresiones futuras cuenten desde cero. Las bobinas y los pesos restantes no cambian.',
usageReset: 'Uso de la bobina restablecido a 0',
allUsageReset: 'Se restablecieron {{count}} bobina(s)',
resetUsageFailed: 'Error al restablecer el uso de la bobina',
resetConsumedCounter: 'Restablecer contador',
resetConsumedCounterTooltip: 'Poner a cero el contador de gramos consumidos de esta bobina. El peso restante no cambia.',
resetConsumedCounterConfirm: '¿Restablecer a 0 el contador de gramos consumidos de esta bobina? Las impresiones futuras volverán a contar desde cero. La bobina en sí, su cálculo de peso restante y sus ajustes no cambian.',
resetAllConsumedCounters: 'Restablecer todos los contadores',
resetAllConsumedCountersTooltip: 'Poner a cero el contador de gramos consumidos de todas las bobinas. Los pesos restantes no cambian.',
resetAllConsumedCountersConfirm: '¿Restablecer a 0 el contador de gramos consumidos de las {{count}} bobinas (incluidas las archivadas)? Esto borra la estadística «Total consumido» para que las impresiones futuras cuenten desde cero. Las bobinas y los pesos restantes no cambian.',
consumedCounterReset: 'Contador restablecido',
allConsumedCountersReset: 'Contador restablecido para {{count}} bobina(s)',
resetConsumedCounterFailed: 'Error al restablecer el contador',
loadedInAms: 'Cargada en AMS/ext.',
remaining: 'Restante',
weightCheck: 'Comprobación de peso',
+9 -9
View File
@@ -3818,15 +3818,15 @@ export default {
inPrinter: 'Dans Imprimante',
lowStock: 'Stock Bas',
sinceTracking: 'Depuis le début du suivi',
resetUsage: 'Réinitialiser l\'usage à 0',
resetUsageTooltip: 'Remettre à zéro le compteur de grammes consommés pour cette bobine',
resetUsageConfirm: 'Remettre à 0 le compteur de grammes consommés de cette bobine ? Les futures impressions repartiront de zéro. La bobine, son calcul de poids restant et vos paramètres ne sont pas modifiés.',
resetAllUsage: 'Réinitialiser l\'usage de toutes les bobines',
resetAllUsageTooltip: 'Remettre à zéro le compteur de grammes consommés sur chaque bobine',
resetAllUsageConfirm: 'Remettre à 0 le compteur de grammes consommés sur les {{count}} bobines (archivées incluses) ? Cela efface la valeur « Total Consommé » pour que les futures impressions repartent de zéro. Les bobines et les poids restants ne sont pas modifiés.',
usageReset: 'Usage de la bobine remis à 0',
allUsageReset: '{{count}} bobine(s) réinitialisée(s)',
resetUsageFailed: 'Échec de la réinitialisation de l\'usage',
resetConsumedCounter: 'Réinitialiser le compteur',
resetConsumedCounterTooltip: 'Remettre à zéro le compteur de grammes consommés pour cette bobine. Le poids restant n\'est pas modifié.',
resetConsumedCounterConfirm: 'Remettre à 0 le compteur de grammes consommés de cette bobine ? Les futures impressions repartiront de zéro. La bobine, son calcul de poids restant et vos paramètres ne sont pas modifiés.',
resetAllConsumedCounters: 'Réinitialiser tous les compteurs',
resetAllConsumedCountersTooltip: 'Remettre à zéro le compteur de grammes consommés sur chaque bobine. Les poids restants ne sont pas modifiés.',
resetAllConsumedCountersConfirm: 'Remettre à 0 le compteur de grammes consommés sur les {{count}} bobines (archivées incluses) ? Cela efface la valeur « Total Consommé » pour que les futures impressions repartent de zéro. Les bobines et les poids restants ne sont pas modifiés.',
consumedCounterReset: 'Compteur réinitialisé',
allConsumedCountersReset: 'Compteur réinitialisé pour {{count}} bobine(s)',
resetConsumedCounterFailed: 'Échec de la réinitialisation du compteur',
loadedInAms: 'Chargé dans AMS/Ext',
remaining: 'Restant',
weightCheck: 'Vérification poids',
+9 -9
View File
@@ -3817,15 +3817,15 @@ export default {
inPrinter: 'In stampante',
lowStock: 'Scorta bassa',
sinceTracking: 'Dall\'inizio del tracciamento',
resetUsage: 'Azzera utilizzo',
resetUsageTooltip: 'Azzera il contatore di grammi consumati per questa bobina',
resetUsageConfirm: 'Azzerare il contatore di grammi consumati di questa bobina? Le stampe future ripartiranno da zero. La bobina, il calcolo del peso rimanente e le impostazioni non vengono modificati.',
resetAllUsage: 'Azzera utilizzo di tutte le bobine',
resetAllUsageTooltip: 'Azzera il contatore di grammi consumati su ogni bobina',
resetAllUsageConfirm: 'Azzerare il contatore di grammi consumati su tutte le {{count}} bobine (incluse quelle archiviate)? La statistica "Totale Consumato" verrà azzerata e le stampe future ripartiranno da zero. Bobine e pesi rimanenti restano invariati.',
usageReset: 'Utilizzo della bobina azzerato',
allUsageReset: '{{count}} bobina/e azzerata/e',
resetUsageFailed: 'Impossibile azzerare l\'utilizzo della bobina',
resetConsumedCounter: 'Azzera contatore',
resetConsumedCounterTooltip: 'Azzera il contatore di grammi consumati per questa bobina. Il peso rimanente non viene modificato.',
resetConsumedCounterConfirm: 'Azzerare il contatore di grammi consumati di questa bobina? Le stampe future ripartiranno da zero. La bobina, il calcolo del peso rimanente e le impostazioni non vengono modificati.',
resetAllConsumedCounters: 'Azzera tutti i contatori',
resetAllConsumedCountersTooltip: 'Azzera il contatore di grammi consumati su ogni bobina. I pesi rimanenti non vengono modificati.',
resetAllConsumedCountersConfirm: 'Azzerare il contatore di grammi consumati su tutte le {{count}} bobine (incluse quelle archiviate)? La statistica "Totale Consumato" verrà azzerata e le stampe future ripartiranno da zero. Bobine e pesi rimanenti restano invariati.',
consumedCounterReset: 'Contatore azzerato',
allConsumedCountersReset: 'Contatore azzerato per {{count}} bobina/e',
resetConsumedCounterFailed: 'Impossibile azzerare il contatore',
loadedInAms: 'Caricato in AMS/Est',
remaining: 'Rimanente',
weightCheck: 'Controllo Peso',
+9 -9
View File
@@ -3829,15 +3829,15 @@ export default {
inPrinter: 'プリンター内',
lowStock: '残量少',
sinceTracking: '追跡開始以降',
resetUsage: '使用量を0にリセット',
resetUsageTooltip: 'このスプールの消費量カウンタを0にする',
resetUsageConfirm: 'このスプールの消費量カウンタを0にリセットしますか?以降の印刷は再びゼロからカウントされます。スプール自体、残量計算、設定は変更されません。',
resetAllUsage: '全スプールの使用量をリセット',
resetAllUsageTooltip: 'すべてのスプールの消費量カウンタを0にする',
resetAllUsageConfirm: '{{count}}件すべてのスプール(アーカイブ済みを含む)の消費量カウンタを0にリセットしますか?「累計消費量」の値がクリアされ、以降の印刷はゼロからカウントされます。スプール自体と残量は変更されません。',
usageReset: 'スプールの使用量を0にリセットしました',
allUsageReset: '{{count}}件のスプールをリセットしました',
resetUsageFailed: 'スプールの使用量リセットに失敗しました',
resetConsumedCounter: 'カウンタをリセット',
resetConsumedCounterTooltip: 'このスプールの消費量カウンタを0にする。残量は変更されません。',
resetConsumedCounterConfirm: 'このスプールの消費量カウンタを0にリセットしますか?以降の印刷は再びゼロからカウントされます。スプール自体、残量計算、設定は変更されません。',
resetAllConsumedCounters: 'すべてのカウンタをリセット',
resetAllConsumedCountersTooltip: 'すべてのスプールの消費量カウンタを0にする。残量は変更されません。',
resetAllConsumedCountersConfirm: '{{count}}件すべてのスプール(アーカイブ済みを含む)の消費量カウンタを0にリセットしますか?「累計消費量」の値がクリアされ、以降の印刷はゼロからカウントされます。スプール自体と残量は変更されません。',
consumedCounterReset: 'カウンタをリセットしました',
allConsumedCountersReset: '{{count}}件のスプールのカウンタをリセットしました',
resetConsumedCounterFailed: 'カウンタのリセットに失敗しました',
loadedInAms: 'AMS/Extに装填中',
remaining: '残り',
weightCheck: '重量チェック',
+9 -9
View File
@@ -3679,15 +3679,15 @@ export default {
spoolmanCatalogLoadFailed: 'Spoolman 필라멘트 카탈로그 불러오기 실패',
subtitle: '스풀 관리',
storageLocationNone: '위치 미설정',
resetUsage: '사용량을 0으로 초기화',
resetUsageTooltip: '이 스풀의 소비된 그램 카운터를 0으로 초기화합니다',
resetUsageConfirm: '이 스풀의 소비된 그램 카운터를 0으로 초기화하시겠습니까? 이후 인쇄는 처음부터 추적됩니다. 스풀 자체, 잔여 무게 계산 및 설정은 변경되지 않습니다.',
resetAllUsage: '모든 스풀 사용량 초기화',
resetAllUsageTooltip: '모든 스풀의 소비된 그램 카운터를 0으로 초기화합니다',
resetAllUsageConfirm: '{{count}}개 스풀(아카이브 포함)의 소비된 그램 카운터를 모두 0으로 초기화하시겠습니까? 이는 "총 소비량" 통계를 지워 이후 인쇄가 처음부터 추적되도록 합니다. 스풀과 잔여 무게는 변경되지 않습니다.',
usageReset: '스풀 사용량이 0으로 초기화되었습니다',
allUsageReset: '{{count}}개 스풀 초기화됨',
resetUsageFailed: '스풀 사용량 초기화 실패'
resetConsumedCounter: '카운터 초기화',
resetConsumedCounterTooltip: '이 스풀의 소비된 그램 카운터를 0으로 초기화합니다. 잔여 무게는 변경되지 않습니다.',
resetConsumedCounterConfirm: '이 스풀의 소비된 그램 카운터를 0으로 초기화하시겠습니까? 이후 인쇄는 처음부터 추적됩니다. 스풀 자체, 잔여 무게 계산 및 설정은 변경되지 않습니다.',
resetAllConsumedCounters: '모든 카운터 초기화',
resetAllConsumedCountersTooltip: '모든 스풀의 소비된 그램 카운터를 0으로 초기화합니다. 잔여 무게는 변경되지 않습니다.',
resetAllConsumedCountersConfirm: '{{count}}개 스풀(아카이브 포함)의 소비된 그램 카운터를 모두 0으로 초기화하시겠습니까? 이는 "총 소비량" 통계를 지워 이후 인쇄가 처음부터 추적되도록 합니다. 스풀과 잔여 무게는 변경되지 않습니다.',
consumedCounterReset: '카운터가 초기화되었습니다',
allConsumedCountersReset: '{{count}}개 스풀의 카운터가 초기화되었습니다',
resetConsumedCounterFailed: '카운터 초기화 실패'
},
timelapse: {
title: '타임랩스',
+9 -9
View File
@@ -3817,15 +3817,15 @@ export default {
inPrinter: 'Na Impressora',
lowStock: 'Estoque Baixo',
sinceTracking: 'Desde o início do rastreamento',
resetUsage: 'Zerar uso',
resetUsageTooltip: 'Zerar o contador de gramas consumidas desta bobina',
resetUsageConfirm: 'Zerar o contador de gramas consumidas desta bobina? As impressões futuras voltarão a contar do zero. A bobina em si, o cálculo do peso restante e as configurações não são alterados.',
resetAllUsage: 'Zerar uso de todas as bobinas',
resetAllUsageTooltip: 'Zerar o contador de gramas consumidas em todas as bobinas',
resetAllUsageConfirm: 'Zerar o contador de gramas consumidas nas {{count}} bobinas (incluindo as arquivadas)? Isso limpa o "Total Consumido" para que as impressões futuras contem do zero. Bobinas e pesos restantes não são alterados.',
usageReset: 'Uso da bobina zerado',
allUsageReset: '{{count}} bobina(s) zerada(s)',
resetUsageFailed: 'Falha ao zerar o uso da bobina',
resetConsumedCounter: 'Zerar contador',
resetConsumedCounterTooltip: 'Zerar o contador de gramas consumidas desta bobina. O peso restante não é alterado.',
resetConsumedCounterConfirm: 'Zerar o contador de gramas consumidas desta bobina? As impressões futuras voltarão a contar do zero. A bobina em si, o cálculo do peso restante e as configurações não são alterados.',
resetAllConsumedCounters: 'Zerar todos os contadores',
resetAllConsumedCountersTooltip: 'Zerar o contador de gramas consumidas em todas as bobinas. Os pesos restantes não são alterados.',
resetAllConsumedCountersConfirm: 'Zerar o contador de gramas consumidas nas {{count}} bobinas (incluindo as arquivadas)? Isso limpa o "Total Consumido" para que as impressões futuras contem do zero. Bobinas e pesos restantes não são alterados.',
consumedCounterReset: 'Contador zerado',
allConsumedCountersReset: 'Contador zerado em {{count}} bobina(s)',
resetConsumedCounterFailed: 'Falha ao zerar o contador',
loadedInAms: 'Carregado no AMS/Ext',
remaining: 'Restante',
weightCheck: 'Verificação de Peso',
+9 -9
View File
@@ -3811,15 +3811,15 @@ export default {
inPrinter: 'Yazıcıda',
lowStock: 'Düşük Stok',
sinceTracking: 'Takip başladığından beri',
resetUsage: 'Kullanımı 0\'a sıfırla',
resetUsageTooltip: 'Bu makara için tüketilen gram sayacını sıfırla',
resetUsageConfirm: 'Bu makaranın tüketilen gram sayacı 0\'a sıfırlansın mı? Gelecekteki baskılar sıfırdan tekrar takip edilecek. Makaranın kendisi, kalan ağırlık hesaplaması ve ayarlarınız değiştirilmez.',
resetAllUsage: 'Tüm makara kullanımını sıfırla',
resetAllUsageTooltip: 'Her makarada tüketilen gram sayacını sıfırla',
resetAllUsageConfirm: 'Tüm {{count}} makaradaki (arşivlenmişler dahil) tüketilen gram sayacı 0\'a sıfırlansın mı? Bu, "Toplam Tüketim" istatistiğini temizler, böylece gelecekteki baskılar sıfırdan takip edilir. Makaralar ve kalan ağırlıklar değiştirilmez.',
usageReset: 'Makara kullanımı 0\'a sıfırlandı',
allUsageReset: '{{count}} makara sıfırlandı',
resetUsageFailed: 'Makara kullanımı sıfırlanamadı',
resetConsumedCounter: 'Sayacı sıfırla',
resetConsumedCounterTooltip: 'Bu makara için tüketilen gram sayacını sıfırla. Kalan ağırlık değişmez.',
resetConsumedCounterConfirm: 'Bu makaranın tüketilen gram sayacı 0\'a sıfırlansın mı? Gelecekteki baskılar sıfırdan tekrar takip edilecek. Makaranın kendisi, kalan ağırlık hesaplaması ve ayarlarınız değiştirilmez.',
resetAllConsumedCounters: 'Tüm sayaçları sıfırla',
resetAllConsumedCountersTooltip: 'Her makarada tüketilen gram sayacını sıfırla. Kalan ağırlıklar değişmez.',
resetAllConsumedCountersConfirm: 'Tüm {{count}} makaradaki (arşivlenmişler dahil) tüketilen gram sayacı 0\'a sıfırlansın mı? Bu, "Toplam Tüketim" istatistiğini temizler, böylece gelecekteki baskılar sıfırdan takip edilir. Makaralar ve kalan ağırlıklar değiştirilmez.',
consumedCounterReset: 'Sayaç sıfırlandı',
allConsumedCountersReset: '{{count}} makaranın sayacı sıfırlandı',
resetConsumedCounterFailed: 'Sayaç sıfırlanamadı',
loadedInAms: 'AMS/Ext\'te Yüklü',
remaining: 'Kalan',
weightCheck: 'Ağırlık Kontrolü',
+9 -9
View File
@@ -3823,15 +3823,15 @@ export default {
inPrinter: '在打印机中',
lowStock: '库存不足',
sinceTracking: '自开始追踪',
resetUsage: '将用量重置为 0',
resetUsageTooltip: '将此料盘的已消耗克数计数器清零',
resetUsageConfirm: '将此料盘的已消耗克数计数器重置为 0?后续打印将从零开始计数。料盘本身、剩余重量计算和您的设置不会改变。',
resetAllUsage: '重置所有料盘的用量',
resetAllUsageTooltip: '将每个料盘的已消耗克数计数器清零',
resetAllUsageConfirm: '将全部 {{count}} 个料盘(含已归档)的已消耗克数计数器重置为 0?这将清空"累计消耗"统计值,后续打印从零开始计数。料盘和剩余重量不会改变。',
usageReset: '料盘用量已重置为 0',
allUsageReset: '已重置 {{count}} 个料盘',
resetUsageFailed: '重置料盘用量失败',
resetConsumedCounter: '重置计数器',
resetConsumedCounterTooltip: '将此料盘的已消耗克数计数器清零。剩余重量保持不变。',
resetConsumedCounterConfirm: '将此料盘的已消耗克数计数器重置为 0?后续打印将从零开始计数。料盘本身、剩余重量计算和您的设置不会改变。',
resetAllConsumedCounters: '重置所有计数器',
resetAllConsumedCountersTooltip: '将每个料盘的已消耗克数计数器清零。剩余重量保持不变。',
resetAllConsumedCountersConfirm: '将全部 {{count}} 个料盘(含已归档)的已消耗克数计数器重置为 0?这将清空"累计消耗"统计值,后续打印从零开始计数。料盘和剩余重量不会改变。',
consumedCounterReset: '计数器已重置',
allConsumedCountersReset: '已重置 {{count}} 个料盘的计数器',
resetConsumedCounterFailed: '重置计数器失败',
loadedInAms: '已装载到 AMS/外置',
remaining: '剩余',
weightCheck: '重量检查',
+9 -9
View File
@@ -3823,15 +3823,15 @@ export default {
inPrinter: '在印表機中',
lowStock: '庫存不足',
sinceTracking: '自開始追蹤',
resetUsage: '將用量重置為 0',
resetUsageTooltip: '將此料盤的已消耗克數計數器歸零',
resetUsageConfirm: '將此料盤的已消耗克數計數器重置為 0?後續列印將從零開始計算。料盤本身、剩餘重量計算與您的設定均不會變更。',
resetAllUsage: '重置所有料盤的用量',
resetAllUsageTooltip: '將每個料盤的已消耗克數計數器歸零',
resetAllUsageConfirm: '將全部 {{count}} 個料盤(含已封存)的已消耗克數計數器重置為 0?這將清空「累計消耗」統計值,後續列印從零開始計算。料盤與剩餘重量不會變更。',
usageReset: '料盤用量已重置為 0',
allUsageReset: '已重置 {{count}} 個料盤',
resetUsageFailed: '重置料盤用量失敗',
resetConsumedCounter: '重置計數器',
resetConsumedCounterTooltip: '將此料盤的已消耗克數計數器歸零。剩餘重量不會變更。',
resetConsumedCounterConfirm: '將此料盤的已消耗克數計數器重置為 0?後續列印將從零開始計算。料盤本身、剩餘重量計算與您的設定均不會變更。',
resetAllConsumedCounters: '重置所有計數器',
resetAllConsumedCountersTooltip: '將每個料盤的已消耗克數計數器歸零。剩餘重量不會變更。',
resetAllConsumedCountersConfirm: '將全部 {{count}} 個料盤(含已封存)的已消耗克數計數器重置為 0?這將清空「累計消耗」統計值,後續列印從零開始計算。料盤與剩餘重量不會變更。',
consumedCounterReset: '計數器已重置',
allConsumedCountersReset: '已重置 {{count}} 個料盤的計數器',
resetConsumedCounterFailed: '重置計數器失敗',
loadedInAms: '已裝載到 AMS/外接',
remaining: '剩餘',
weightCheck: '重量檢查',
+35 -31
View File
@@ -467,8 +467,8 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
const [formModal, setFormModal] = useState<{ spool?: InventorySpool | null; mode: SpoolFormMode } | null>(null);
const deepLinkHandled = useRef(false);
const [confirmAction, setConfirmAction] = useState<
| { type: 'delete' | 'archive' | 'reset-usage'; spoolId: number }
| { type: 'reset-all-usage' }
| { type: 'delete' | 'archive' | 'reset-consumed-counter'; spoolId: number }
| { type: 'reset-all-consumed-counters' }
| null
>(null);
// Label printing (#809). null = closed; otherwise the IDs to print labels for.
@@ -687,27 +687,31 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
},
});
const resetUsageMutation = useMutation({
const resetConsumedCounterMutation = useMutation({
mutationFn: (id: number) =>
spoolmanMode ? api.resetSpoolmanInventorySpoolUsage(id) : api.resetSpoolUsage(id),
spoolmanMode
? api.resetSpoolmanInventorySpoolConsumedCounter(id)
: api.resetSpoolConsumedCounter(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
showToast(t('inventory.usageReset'), 'success');
showToast(t('inventory.consumedCounterReset'), 'success');
},
onError: () => {
showToast(t('inventory.resetUsageFailed'), 'error');
showToast(t('inventory.resetConsumedCounterFailed'), 'error');
},
});
const bulkResetUsageMutation = useMutation({
const bulkResetConsumedCounterMutation = useMutation({
mutationFn: (ids: number[]) =>
spoolmanMode ? api.bulkResetSpoolmanInventorySpoolUsage(ids) : api.bulkResetSpoolUsage(ids),
spoolmanMode
? api.bulkResetSpoolmanInventorySpoolConsumedCounter(ids)
: api.bulkResetSpoolConsumedCounter(ids),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: spoolsQueryKey });
showToast(t('inventory.allUsageReset', { count: data.reset }), 'success');
showToast(t('inventory.allConsumedCountersReset', { count: data.reset }), 'success');
},
onError: () => {
showToast(t('inventory.resetUsageFailed'), 'error');
showToast(t('inventory.resetConsumedCounterFailed'), 'error');
},
});
@@ -1145,10 +1149,10 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
</div>
{stats.totalConsumed > 0 && resetableSpoolIds.length > 0 && (
<button
onClick={() => setConfirmAction({ type: 'reset-all-usage' })}
onClick={() => setConfirmAction({ type: 'reset-all-consumed-counters' })}
className="p-1 text-bambu-gray hover:text-red-400 rounded transition-colors"
title={t('inventory.resetAllUsageTooltip')}
aria-label={t('inventory.resetAllUsage')}
title={t('inventory.resetAllConsumedCountersTooltip')}
aria-label={t('inventory.resetAllConsumedCounters')}
>
<Eraser className="w-3.5 h-3.5" />
</button>
@@ -1747,7 +1751,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
onArchive={(id) => setConfirmAction({ type: 'archive', spoolId: id })}
onDelete={(id) => setConfirmAction({ type: 'delete', spoolId: id })}
onPrintLabel={(id) => setLabelPickerSpoolIds([id])}
onResetUsage={(id) => setConfirmAction({ type: 'reset-usage', spoolId: id })}
onResetConsumedCounter={(id) => setConfirmAction({ type: 'reset-consumed-counter', spoolId: id })}
visibleColumns={visibleColumns}
assignmentMap={assignmentMap}
catalogMap={catalogMap}
@@ -1773,7 +1777,7 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
onArchive={() => setConfirmAction({ type: 'archive', spoolId: spool.id })}
onDelete={() => setConfirmAction({ type: 'delete', spoolId: spool.id })}
onPrintLabel={() => setLabelPickerSpoolIds([spool.id])}
onResetUsage={() => setConfirmAction({ type: 'reset-usage', spoolId: spool.id })}
onResetConsumedCounter={() => setConfirmAction({ type: 'reset-consumed-counter', spoolId: spool.id })}
visibleColumns={visibleColumns}
assignmentMap={assignmentMap}
catalogMap={catalogMap}
@@ -1874,25 +1878,25 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
/>
)}
{/* Confirm Modal (delete / archive / reset-usage / reset-all-usage) */}
{/* Confirm Modal (delete / archive / reset-consumed-counter / reset-all-consumed-counters) */}
{confirmAction && (
<ConfirmModal
title={
confirmAction.type === 'delete' ? t('common.delete') :
confirmAction.type === 'archive' ? t('inventory.archive') :
confirmAction.type === 'reset-usage' ? t('inventory.resetUsage') :
t('inventory.resetAllUsage')
confirmAction.type === 'reset-consumed-counter' ? t('inventory.resetConsumedCounter') :
t('inventory.resetAllConsumedCounters')
}
message={
confirmAction.type === 'delete' ? t('inventory.deleteConfirm') :
confirmAction.type === 'archive' ? t('inventory.archiveConfirm') :
confirmAction.type === 'reset-usage' ? t('inventory.resetUsageConfirm') :
t('inventory.resetAllUsageConfirm', { count: resetableSpoolIds.length })
confirmAction.type === 'reset-consumed-counter' ? t('inventory.resetConsumedCounterConfirm') :
t('inventory.resetAllConsumedCountersConfirm', { count: resetableSpoolIds.length })
}
confirmText={
confirmAction.type === 'delete' ? t('common.delete') :
confirmAction.type === 'archive' ? t('inventory.archive') :
t('inventory.resetUsage')
t('inventory.resetConsumedCounter')
}
variant={confirmAction.type === 'archive' ? 'warning' : 'danger'}
onConfirm={() => {
@@ -1900,10 +1904,10 @@ function InventoryPage({ spoolmanMode = false, spoolmanModeReady = true }: { spo
deleteMutation.mutate(confirmAction.spoolId);
} else if (confirmAction.type === 'archive') {
archiveMutation.mutate(confirmAction.spoolId);
} else if (confirmAction.type === 'reset-usage') {
resetUsageMutation.mutate(confirmAction.spoolId);
} else if (confirmAction.type === 'reset-consumed-counter') {
resetConsumedCounterMutation.mutate(confirmAction.spoolId);
} else {
bulkResetUsageMutation.mutate(resetableSpoolIds);
bulkResetConsumedCounterMutation.mutate(resetableSpoolIds);
}
setConfirmAction(null);
}}
@@ -2115,7 +2119,7 @@ function SpoolCard({
/* Single spool row for table view */
function SpoolTableRow({
spool, remaining, pct, onEdit, onCopy, onRestore, onArchive, onDelete, onPrintLabel, onResetUsage,
spool, remaining, pct, onEdit, onCopy, onRestore, onArchive, onDelete, onPrintLabel, onResetConsumedCounter,
visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
}: {
spool: InventorySpool;
@@ -2127,7 +2131,7 @@ function SpoolTableRow({
onArchive: () => void;
onDelete: () => void;
onPrintLabel?: () => void;
onResetUsage?: () => void;
onResetConsumedCounter?: () => void;
visibleColumns: string[];
assignmentMap: Record<number, LocationDisplay>;
catalogMap: Record<number, SpoolCatalogEntry>;
@@ -2163,12 +2167,12 @@ function SpoolTableRow({
<Printer className="w-4 h-4" />
</button>
)}
{onResetUsage && spool.weight_used > 0 && (
{onResetConsumedCounter && spool.weight_used > 0 && (
// Eraser also shows on archived spools (#1390 follow-up):
// archived consumed weight now counts in "Total Consumed", so
// the user needs a way to zero an archived spool's tracking
// counter individually without having to un-archive it first.
<button onClick={onResetUsage} className="p-1.5 text-bambu-gray hover:text-orange-400 rounded transition-colors" title={t('inventory.resetUsageTooltip')}>
<button onClick={onResetConsumedCounter} className="p-1.5 text-bambu-gray hover:text-orange-400 rounded transition-colors" title={t('inventory.resetConsumedCounterTooltip')}>
<Eraser className="w-4 h-4" />
</button>
)}
@@ -2193,7 +2197,7 @@ function SpoolTableRow({
/* Grouped spool rows for table view */
function SpoolTableGroup({
spools, headerSpool, remaining, pct, isExpanded, onToggle,
onEdit, onCopy, onArchive, onDelete, onPrintLabel, onResetUsage,
onEdit, onCopy, onArchive, onDelete, onPrintLabel, onResetConsumedCounter,
visibleColumns, assignmentMap, catalogMap, currencySymbol, dateFormat, t, onSyncWeight,
}: {
spools: InventorySpool[];
@@ -2209,7 +2213,7 @@ function SpoolTableGroup({
onArchive: (id: number) => void;
onDelete: (id: number) => void;
onPrintLabel?: (spoolId: number) => void;
onResetUsage?: (id: number) => void;
onResetConsumedCounter?: (id: number) => void;
visibleColumns: string[];
assignmentMap: Record<number, LocationDisplay>;
catalogMap: Record<number, SpoolCatalogEntry>;
@@ -2263,7 +2267,7 @@ function SpoolTableGroup({
onArchive={() => onArchive(spool.id)}
onDelete={() => onDelete(spool.id)}
onPrintLabel={onPrintLabel ? () => onPrintLabel(spool.id) : undefined}
onResetUsage={onResetUsage ? () => onResetUsage(spool.id) : undefined}
onResetConsumedCounter={onResetConsumedCounter ? () => onResetConsumedCounter(spool.id) : undefined}
visibleColumns={visibleColumns}
assignmentMap={assignmentMap}
catalogMap={catalogMap}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-6Wj7SYfZ.js"></script>
<script type="module" crossorigin src="/assets/index-C7bMJs5u.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Df3XYvpK.css">
</head>
<body>