Jelajahi Sumber

Say why a preset's values are unavailable, not just that they are

The settings panel collapsed four causes into one message -- "the picked
preset's own values could not be read" -- with no indication of what to do
about it.

The overwhelmingly common cause has an obvious fix, and it isn't an edge
case: an install pulls its sidecar as SIDECAR_TAG:-latest regardless of
which Bambuddy channel it is on, so a current Bambuddy talking to a
sidecar that predates POST /profiles/resolve is the normal state, not a
misconfiguration. Those users would have seen an amber warning on every
slice with nothing pointing at the sidecar image.

resolve_profile now returns ResolvedProfile(values, reason) instead of
None for everything, the route passes the reason through, and the panel
picks its message from it:

  sidecar_outdated     -> name the fix: update the sidecar image
  sidecar_unavailable  -> the sidecar did not answer
  not_configured       -> no sidecar is configured
  preset_unresolved    -> the previous generic wording

A request that fails outright maps to sidecar_unavailable, since a
backend we cannot reach and a sidecar that will not answer are the same
thing from the dialog.

Every variant still ends with "anything you don't change still uses the
preset" -- that reassurance is the point of the notice, and it is true
whichever way the lookup failed.

Tests pin the distinction rather than just the happy path: a 404 and a
500 must produce different reasons, and each panel case asserts both that
its own message appears and that the "update the sidecar image" line does
not leak into the others.
maziggy 4 minggu lalu
induk
melakukan
b6027b7138

File diff ditekan karena terlalu besar
+ 0 - 0
CHANGELOG.md


+ 17 - 12
backend/app/api/routes/slicer_presets.py

@@ -563,40 +563,45 @@ async def get_preset_values(
     OrcaSlicer's published tree instead, which can disagree with what actually
     slices; showing numbers from it would be confidently wrong.
 
-    Returns ``{"resolved": false, "values": {}}`` rather than an error whenever
-    the values can't be obtained (sidecar offline, too old for the endpoint, or
-    slicing not configured). The panel then falls back to schema defaults and
-    tells the user the values are indicative, which is a far better outcome
-    than a modal that won't open.
+    Returns ``{"resolved": false, "values": {}, "reason": "..."}`` rather than
+    an error whenever the values can't be obtained. ``reason`` is what makes
+    the fallback actionable: a Bambuddy install pulls its sidecar as
+    ``SIDECAR_TAG:-latest`` regardless of its own release channel, so the
+    overwhelmingly common cause is a sidecar older than the endpoint — which
+    the user fixes by pulling a newer image, if we tell them that instead of
+    "could not read the values".
     """
     if slot != "process":
         raise HTTPException(status_code=400, detail="Only the 'process' slot is supported")
 
     ref = PresetRef(source=source, id=id)
 
+    def unresolved(reason: str) -> dict:
+        return {"resolved": False, "values": {}, "reason": reason}
+
     try:
         profile_json = await resolve_preset_ref(db, current_user, ref, slot)
     except HTTPException:
         # A preset the caller can't resolve is not a reason to break the panel;
         # the slice itself will report it properly if they go ahead.
         logger.info("Could not resolve %s preset %s for value lookup", slot, id)
-        return {"resolved": False, "values": {}}
+        return unresolved("preset_unresolved")
 
     api_url = await _resolve_slicer_api_url(db)
     if not api_url:
-        return {"resolved": False, "values": {}}
+        return unresolved("not_configured")
 
     service = SlicerApiService(api_url)
     try:
-        values = await service.resolve_profile(profile_json, "process")
+        resolved = await service.resolve_profile(profile_json, "process")
     except SlicerApiUnavailableError:
-        return {"resolved": False, "values": {}}
+        return unresolved("sidecar_unavailable")
     finally:
         await service.close()
 
-    if values is None:
-        return {"resolved": False, "values": {}}
-    return {"resolved": True, "values": values}
+    if resolved.values is None:
+        return unresolved(resolved.reason)
+    return {"resolved": True, "values": resolved.values, "reason": "ok"}
 
 
 @router.get("/presets", response_model=UnifiedPresetsResponse)

+ 30 - 10
backend/app/services/slicer_api.py

@@ -54,6 +54,18 @@ class SlicerTimeoutError(SlicerApiError):
     """
 
 
+class ResolvedProfile(NamedTuple):
+    """A preset's effective values, or why they are unavailable.
+
+    ``reason`` is one of ``ok`` / ``sidecar_outdated`` / ``sidecar_unavailable``
+    / ``preset_unresolved``. It exists so the UI can say something actionable
+    instead of one generic "could not read the values" for four causes.
+    """
+
+    values: dict | None
+    reason: str
+
+
 class SliceResult(NamedTuple):
     """Result of a slice operation."""
 
@@ -310,7 +322,7 @@ class SlicerApiService:
             raise SlicerApiUnavailableError(f"Slicer sidecar /health returned {response.status_code}")
         return response.json()
 
-    async def resolve_profile(self, profile_json: str, category: str) -> dict | None:
+    async def resolve_profile(self, profile_json: str, category: str) -> "ResolvedProfile":
         """POST /profiles/resolve — flatten a preset's ``inherits:`` chain.
 
         Returns the effective key/value map the slicer would actually use, so
@@ -325,15 +337,19 @@ class SlicerApiService:
         the one baked into the running sidecar image — values from it would look
         authoritative and could quietly disagree with what gets sliced.
 
-        Returns ``None`` when the sidecar is too old to have the endpoint, so
-        callers can degrade to schema defaults instead of failing the modal.
-        Genuine transport failures still raise.
+        Returns a :class:`ResolvedProfile` whose ``reason`` distinguishes *why*
+        values are missing. That matters more than it looks: the common case in
+        practice is a sidecar older than this endpoint, because a Bambuddy
+        install pulls ``SIDECAR_TAG:-latest`` independently of its own release
+        channel. "Could not read the values" sends that user hunting; "your
+        sidecar image is older than this feature" is a one-line fix. Genuine
+        transport failures still raise.
         """
         try:
             payload = json.loads(profile_json)
         except json.JSONDecodeError:
             logger.warning("Cannot resolve %s preset: content is not valid JSON", category)
-            return None
+            return ResolvedProfile(None, "preset_unresolved")
 
         try:
             response = await self._client.post(
@@ -345,21 +361,25 @@ class SlicerApiService:
             raise SlicerApiUnavailableError(f"Slicer sidecar unreachable: {exc}") from exc
 
         if response.status_code == 404:
-            # Sidecar predates the endpoint. Not an error — the caller shows
-            # schema defaults and says so.
+            # Sidecar predates the endpoint. Not an error, and specifically not
+            # the same as a broken one — this is the case that has a fix the
+            # user can act on.
             logger.info("Slicer sidecar has no /profiles/resolve; falling back to schema defaults")
-            return None
+            return ResolvedProfile(None, "sidecar_outdated")
         if response.status_code >= 400:
             logger.warning(
                 "Slicer sidecar /profiles/resolve returned %s: %s",
                 response.status_code,
                 _format_sidecar_error(response),
             )
-            return None
+            return ResolvedProfile(None, "sidecar_unavailable")
 
         body = response.json()
         resolved = body.get("profile") if isinstance(body, dict) else None
-        return resolved if isinstance(resolved, dict) else None
+        if not isinstance(resolved, dict):
+            logger.warning("Slicer sidecar /profiles/resolve returned no profile object")
+            return ResolvedProfile(None, "sidecar_unavailable")
+        return ResolvedProfile(resolved, "ok")
 
     async def list_bundled_profiles(self) -> dict:
         """GET /profiles/bundled — return the slicer's stock profiles by slot.

+ 25 - 15
backend/tests/unit/test_slicer_preset_values.py

@@ -34,22 +34,28 @@ class TestResolveProfile:
             return httpx.Response(200, json={"profile": {"line_width": "0.42", "wall_loops": "2"}})
 
         service = _service(handler)
-        assert await service.resolve_profile(PROCESS_STUB, "process") == {
-            "line_width": "0.42",
-            "wall_loops": "2",
-        }
+        result = await service.resolve_profile(PROCESS_STUB, "process")
+        assert result.values == {"line_width": "0.42", "wall_loops": "2"}
+        assert result.reason == "ok"
 
     @pytest.mark.asyncio
-    async def test_a_sidecar_without_the_endpoint_is_not_an_error(self):
-        # Older images 404 here. The panel degrades to schema defaults and says
-        # so; failing would make the modal unusable against an old sidecar.
+    async def test_a_sidecar_without_the_endpoint_is_reported_as_outdated(self):
+        # Older images 404 here. This is the dominant case in practice -- an
+        # install pulls SIDECAR_TAG:-latest regardless of its own release
+        # channel -- and it is the one with a fix the user can act on, so it
+        # must not be flattened into the generic failure.
         service = _service(lambda request: httpx.Response(404, json={"message": "Not Found"}))
-        assert await service.resolve_profile(PROCESS_STUB, "process") is None
+        result = await service.resolve_profile(PROCESS_STUB, "process")
+        assert result.values is None
+        assert result.reason == "sidecar_outdated"
 
     @pytest.mark.asyncio
-    async def test_a_sidecar_error_degrades_rather_than_raising(self):
+    async def test_a_sidecar_error_is_not_reported_as_outdated(self):
+        # A broken sidecar and an old one call for different advice.
         service = _service(lambda request: httpx.Response(500, json={"message": "boom"}))
-        assert await service.resolve_profile(PROCESS_STUB, "process") is None
+        result = await service.resolve_profile(PROCESS_STUB, "process")
+        assert result.values is None
+        assert result.reason == "sidecar_unavailable"
 
     @pytest.mark.asyncio
     async def test_unreachable_sidecar_still_raises(self):
@@ -62,15 +68,19 @@ class TestResolveProfile:
             await _service(handler).resolve_profile(PROCESS_STUB, "process")
 
     @pytest.mark.asyncio
-    async def test_unparseable_preset_content_returns_none(self):
+    async def test_unparseable_preset_content_blames_the_preset(self):
         service = _service(lambda request: httpx.Response(200, json={"profile": {}}))
-        assert await service.resolve_profile("not json", "process") is None
+        result = await service.resolve_profile("not json", "process")
+        assert result.values is None
+        assert result.reason == "preset_unresolved"
 
     @pytest.mark.asyncio
-    async def test_a_response_without_a_profile_object_returns_none(self):
+    async def test_a_response_without_a_profile_object_returns_no_values(self):
         # Guards against reading a differently-shaped body as if it were values.
         service = _service(lambda request: httpx.Response(200, json={"ok": True}))
-        assert await service.resolve_profile(PROCESS_STUB, "process") is None
+        result = await service.resolve_profile(PROCESS_STUB, "process")
+        assert result.values is None
+        assert result.reason == "sidecar_unavailable"
 
     @pytest.mark.asyncio
     async def test_an_already_flat_preset_round_trips(self):
@@ -81,7 +91,7 @@ class TestResolveProfile:
             assert "inherits" not in body["profile"]
             return httpx.Response(200, json={"profile": json.loads(flat)})
 
-        assert await _service(handler).resolve_profile(flat, "process") == {
+        assert (await _service(handler).resolve_profile(flat, "process")).values == {
             "line_width": "0.45",
             "type": "process",
         }

+ 3 - 3
frontend/src/__tests__/components/SliceModal.test.tsx

@@ -105,7 +105,7 @@ describe('SliceModal', () => {
   beforeEach(() => {
     vi.clearAllMocks();
     mockApi.getSlicerPresets.mockResolvedValue(fullThreeTier);
-    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {} });
+    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {}, reason: 'ok' });
     mockApi.getSliceJob.mockResolvedValue({
       job_id: 42,
       status: 'running',
@@ -1559,7 +1559,7 @@ describe('SliceModal — process settings in "slice as designed" mode', () => {
   beforeEach(() => {
     vi.clearAllMocks();
     mockApi.getSlicerPresets.mockResolvedValue(fullThreeTier);
-    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {} });
+    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {}, reason: 'ok' });
     mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
     mockApi.getSlicerPrinterModels.mockResolvedValue({});
     mockApi.getLibraryFilePlates.mockResolvedValue({
@@ -1643,7 +1643,7 @@ describe('SliceModal — process settings layout', () => {
   beforeEach(() => {
     vi.clearAllMocks();
     mockApi.getSlicerPresets.mockResolvedValue(fullThreeTier);
-    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {} });
+    mockApi.getSlicerPresetValues.mockResolvedValue({ resolved: true, values: {}, reason: 'ok' });
     mockApi.listSlicerPipelines.mockResolvedValue({ pipelines: [] });
     mockApi.getLibraryFilePlates.mockResolvedValue({
       file_id: 100,

+ 25 - 0
frontend/src/__tests__/components/SlicerSettingsPanel.test.tsx

@@ -7,6 +7,7 @@ import { render } from '../utils';
 import SlicerSettingsPanel, { type FilamentChoice } from '../../components/SlicerSettingsPanel';
 import type { SettingValue } from '../../types/slicerSettings';
 import type { DesignOverride } from '../../types/plates';
+import type { SlicerPresetValuesReason } from '../../api/client';
 
 /**
  * The panel is a controlled component: it renders from the `values` prop and
@@ -22,6 +23,7 @@ function Harness({
   filamentChoices,
   presetValues,
   presetValuesResolved,
+  presetValuesReason,
 }: {
   initial: Record<string, SettingValue>;
   onChange: (v: Record<string, SettingValue>, s: Record<string, string | string[]>) => void;
@@ -30,6 +32,7 @@ function Harness({
   filamentChoices?: FilamentChoice[];
   presetValues?: Record<string, SettingValue>;
   presetValuesResolved?: boolean;
+  presetValuesReason?: SlicerPresetValuesReason;
 }) {
   const [values, setValues] = useState(initial);
   const [selected, setSelected] = useState(new Set(initialSelected ?? []));
@@ -43,6 +46,7 @@ function Harness({
       filamentChoices={filamentChoices}
       presetValues={presetValues}
       presetValuesResolved={presetValuesResolved}
+      presetValuesReason={presetValuesReason}
       sourceOverrides={sourceOverrides}
       sourceSelected={selected}
       onToggleSource={(key, on) =>
@@ -66,6 +70,7 @@ async function renderPanel(
     filamentChoices?: FilamentChoice[];
     presetValues?: Record<string, SettingValue>;
     presetValuesResolved?: boolean;
+    presetValuesReason?: SlicerPresetValuesReason;
   } = {},
 ) {
   const onChange = vi.fn();
@@ -447,6 +452,26 @@ describe('SlicerSettingsPanel — the picked preset\'s values', () => {
     await waitFor(() => expect(screen.getByText(/Showing slicer defaults/)).toBeInTheDocument());
   });
 
+  it('names the fix when the sidecar predates the endpoint', async () => {
+    // The dominant case: an install pulls SIDECAR_TAG:-latest regardless of
+    // its own release channel, so a current Bambuddy against an old sidecar is
+    // normal. A generic "could not be read" sends that user hunting.
+    await renderPanel({}, { presetValuesResolved: false, presetValuesReason: 'sidecar_outdated' });
+    await waitFor(() => expect(screen.getByText(/Update the sidecar image/)).toBeInTheDocument());
+  });
+
+  it('distinguishes a sidecar that is missing from one that is merely old', async () => {
+    await renderPanel({}, { presetValuesResolved: false, presetValuesReason: 'not_configured' });
+    await waitFor(() => expect(screen.getByText(/no slicer sidecar is configured/)).toBeInTheDocument());
+    expect(screen.queryByText(/Update the sidecar image/)).not.toBeInTheDocument();
+  });
+
+  it('distinguishes a sidecar that did not answer', async () => {
+    await renderPanel({}, { presetValuesResolved: false, presetValuesReason: 'sidecar_unavailable' });
+    await waitFor(() => expect(screen.getByText(/did not answer/)).toBeInTheDocument());
+    expect(screen.queryByText(/Update the sidecar image/)).not.toBeInTheDocument();
+  });
+
   it('shows no such notice when they resolved', async () => {
     await renderPanel({}, { presetValues: { line_width: '0.42' }, presetValuesResolved: true });
     await waitFor(() => expect(screen.getByPlaceholderText('Search settings')).toBeInTheDocument());

+ 17 - 0
frontend/src/api/client.ts

@@ -1614,11 +1614,28 @@ export interface PresetRef {
   source: PresetSource;
   id: string;
 }
+/**
+ * Why a preset's effective values are unavailable.
+ *
+ * `sidecar_outdated` is the one that matters in practice: an install pulls its
+ * sidecar as `SIDECAR_TAG:-latest` regardless of which Bambuddy channel it is
+ * on, so a user can perfectly well be running a current Bambuddy against a
+ * sidecar that predates this endpoint. That has a one-line fix, and saying so
+ * beats a generic "could not read the values".
+ */
+export type SlicerPresetValuesReason =
+  | 'ok'
+  | 'sidecar_outdated'
+  | 'sidecar_unavailable'
+  | 'not_configured'
+  | 'preset_unresolved';
+
 export interface SlicerPresetValues {
   /** False when the sidecar could not supply values; `values` is then empty. */
   resolved: boolean;
   /** Flattened key -> value map, in the string forms a process preset stores. */
   values: Record<string, string | string[]>;
+  reason: SlicerPresetValuesReason;
 }
 
 export interface SliceRequest {

+ 5 - 0
frontend/src/components/SliceModal.tsx

@@ -440,6 +440,10 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
   // rather than blocking the panel on it.
   const presetValues = presetValuesQuery.data?.values as Record<string, SettingValue> | undefined;
   const presetValuesResolved = presetValuesQuery.data?.resolved ?? presetValuesQuery.isLoading;
+  // A failed request (rather than a 'resolved: false' answer) means we
+  // never reached the backend, which is the same situation as an
+  // unreachable sidecar as far as the user is concerned.
+  const presetValuesReason = presetValuesQuery.data?.reason ?? (presetValuesQuery.isError ? 'sidecar_unavailable' : undefined);
 
   // Slot list for the settings panel's filament pickers (support base and
   // interface, and the Multimaterial page's per-region options). Those store a
@@ -1051,6 +1055,7 @@ export function SliceModal({ source, onClose }: SliceModalProps) {
                           filamentChoices={filamentChoices}
                           presetValues={presetValues}
                           presetValuesResolved={presetValuesResolved}
+                          presetValuesReason={presetValuesReason}
                           sourceOverrides={designOverrides}
                           sourceSelected={designKeys}
                           onToggleSource={(key, on) =>

+ 26 - 4
frontend/src/components/SlicerSettingsPanel.tsx

@@ -26,6 +26,7 @@ import { disabledKeys, type ToggleRules } from '../lib/slicerToggle';
 import { baselineForDisplay, displaySidetext, isModified, numericBound, serializeOverrides } from '../lib/slicerSettings';
 import type { OptionMode, ProcessOption, ProcessSchema, ProcessUiTree, SettingValue } from '../types/slicerSettings';
 import type { DesignOverride } from '../types/plates';
+import type { SlicerPresetValuesReason } from '../api/client';
 
 interface SlicerData {
   schema: ProcessSchema;
@@ -81,6 +82,11 @@ interface Props {
   presetValues?: Record<string, SettingValue>;
   /** False when the preset's values could not be fetched. */
   presetValuesResolved?: boolean;
+  /**
+   * Why they could not be fetched, so the notice can name a fix. Left
+   * unset while the fetch is still in flight.
+   */
+  presetValuesReason?: SlicerPresetValuesReason;
 }
 
 export interface FilamentChoice {
@@ -123,6 +129,7 @@ export default function SlicerSettingsPanel({
   filamentChoices,
   presetValues,
   presetValuesResolved = true,
+  presetValuesReason,
 }: Props) {
   const { t } = useTranslation();
   const [data, setData] = useState<SlicerData | null>(null);
@@ -288,10 +295,25 @@ export default function SlicerSettingsPanel({
 
       {!presetValuesResolved && (
         <p className="rounded border border-amber-300 bg-amber-50 px-2 py-1 text-[0.7rem] text-amber-800 dark:border-amber-700/40 dark:bg-amber-900/20 dark:text-amber-200">
-          {t(
-            'slicerSettings.presetValuesUnavailable',
-            "Showing slicer defaults: the picked preset's own values could not be read. Anything you don't change still uses the preset.",
-          )}
+          {presetValuesReason === 'sidecar_outdated'
+            ? t(
+                'slicerSettings.presetValuesOutdatedSidecar',
+                "Showing slicer defaults: your slicer sidecar is older than this feature and can't report a preset's values. Update the sidecar image to see them. Anything you don't change still uses the preset.",
+              )
+            : presetValuesReason === 'not_configured'
+              ? t(
+                  'slicerSettings.presetValuesNotConfigured',
+                  "Showing slicer defaults: no slicer sidecar is configured, so a preset's values can't be read. Anything you don't change still uses the preset.",
+                )
+              : presetValuesReason === 'sidecar_unavailable'
+                ? t(
+                    'slicerSettings.presetValuesSidecarUnavailable',
+                    "Showing slicer defaults: the slicer sidecar did not answer, so a preset's values can't be read. Anything you don't change still uses the preset.",
+                  )
+                : t(
+                    'slicerSettings.presetValuesUnavailable',
+                    "Showing slicer defaults: the picked preset's own values could not be read. Anything you don't change still uses the preset.",
+                  )}
         </p>
       )}
 

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -4279,6 +4279,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Es werden Slicer-Standardwerte angezeigt: Ihr Slicer-Sidecar ist älter als diese Funktion und kann die Werte eines Profils nicht liefern. Aktualisieren Sie das Sidecar-Image, um sie zu sehen. Alles, was Sie nicht ändern, verwendet weiterhin das Profil.',
+    presetValuesNotConfigured: 'Es werden Slicer-Standardwerte angezeigt: Es ist kein Slicer-Sidecar konfiguriert, daher können die Werte eines Profils nicht gelesen werden. Alles, was Sie nicht ändern, verwendet weiterhin das Profil.',
+    presetValuesSidecarUnavailable: 'Es werden Slicer-Standardwerte angezeigt: Das Slicer-Sidecar hat nicht geantwortet, daher können die Werte eines Profils nicht gelesen werden. Alles, was Sie nicht ändern, verwendet weiterhin das Profil.',
     presetValuesUnavailable: 'Es werden Slicer-Standardwerte angezeigt: Die Werte des gewählten Profils konnten nicht gelesen werden. Alles, was Sie nicht ändern, verwendet weiterhin das Profil.',
     filamentDefault: 'Standard',
     fromFile: 'aus Datei',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -4313,6 +4313,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: "Showing slicer defaults: your slicer sidecar is older than this feature and can't report a preset's values. Update the sidecar image to see them. Anything you don't change still uses the preset.",
+    presetValuesNotConfigured: "Showing slicer defaults: no slicer sidecar is configured, so a preset's values can't be read. Anything you don't change still uses the preset.",
+    presetValuesSidecarUnavailable: "Showing slicer defaults: the slicer sidecar did not answer, so a preset's values can't be read. Anything you don't change still uses the preset.",
     presetValuesUnavailable: "Showing slicer defaults: the picked preset's own values could not be read. Anything you don't change still uses the preset.",
     filamentDefault: 'Default',
     fromFile: 'from file',

+ 3 - 0
frontend/src/i18n/locales/es.ts

@@ -4281,6 +4281,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Se muestran los valores predeterminados del laminador: tu sidecar es más antiguo que esta función y no puede informar los valores de un perfil. Actualiza la imagen del sidecar para verlos. Todo lo que no cambies seguirá usando el perfil.',
+    presetValuesNotConfigured: 'Se muestran los valores predeterminados del laminador: no hay ningún sidecar configurado, así que no se pueden leer los valores de un perfil. Todo lo que no cambies seguirá usando el perfil.',
+    presetValuesSidecarUnavailable: 'Se muestran los valores predeterminados del laminador: el sidecar no respondió, así que no se pueden leer los valores de un perfil. Todo lo que no cambies seguirá usando el perfil.',
     presetValuesUnavailable: 'Se muestran los valores predeterminados del laminador: no se pudieron leer los del perfil seleccionado. Todo lo que no cambies seguirá usando el perfil.',
     filamentDefault: 'Predeterminado',
     fromFile: 'del archivo',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -4268,6 +4268,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: "Valeurs par défaut du trancheur affichées : votre sidecar est plus ancien que cette fonctionnalité et ne peut pas fournir les valeurs d'un profil. Mettez à jour l'image du sidecar pour les voir. Tout ce que vous ne modifiez pas utilise toujours le profil.",
+    presetValuesNotConfigured: "Valeurs par défaut du trancheur affichées : aucun sidecar n'est configuré, les valeurs d'un profil ne peuvent donc pas être lues. Tout ce que vous ne modifiez pas utilise toujours le profil.",
+    presetValuesSidecarUnavailable: "Valeurs par défaut du trancheur affichées : le sidecar n'a pas répondu, les valeurs d'un profil ne peuvent donc pas être lues. Tout ce que vous ne modifiez pas utilise toujours le profil.",
     presetValuesUnavailable: "Valeurs par défaut du trancheur affichées : celles du profil choisi n'ont pas pu être lues. Tout ce que vous ne modifiez pas utilise toujours le profil.",
     filamentDefault: 'Par défaut',
     fromFile: 'du fichier',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -4267,6 +4267,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: "Sono mostrati i valori predefiniti dello slicer: il tuo sidecar è più vecchio di questa funzione e non può fornire i valori di un profilo. Aggiorna l'immagine del sidecar per vederli. Tutto ciò che non modifichi continua a usare il profilo.",
+    presetValuesNotConfigured: 'Sono mostrati i valori predefiniti dello slicer: nessun sidecar è configurato, quindi i valori di un profilo non sono leggibili. Tutto ciò che non modifichi continua a usare il profilo.',
+    presetValuesSidecarUnavailable: 'Sono mostrati i valori predefiniti dello slicer: il sidecar non ha risposto, quindi i valori di un profilo non sono leggibili. Tutto ciò che non modifichi continua a usare il profilo.',
     presetValuesUnavailable: 'Sono mostrati i valori predefiniti dello slicer: quelli del profilo scelto non sono leggibili. Tutto ciò che non modifichi continua a usare il profilo.',
     filamentDefault: 'Predefinito',
     fromFile: 'dal file',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -4279,6 +4279,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'スライサーの既定値を表示しています。スライサーサイドカーがこの機能より古く、プリセットの値を取得できません。値を表示するにはサイドカーのイメージを更新してください。変更しない項目は引き続きプリセットの値が使われます。',
+    presetValuesNotConfigured: 'スライサーの既定値を表示しています。スライサーサイドカーが設定されていないため、プリセットの値を読み取れません。変更しない項目は引き続きプリセットの値が使われます。',
+    presetValuesSidecarUnavailable: 'スライサーの既定値を表示しています。スライサーサイドカーが応答しなかったため、プリセットの値を読み取れません。変更しない項目は引き続きプリセットの値が使われます。',
     presetValuesUnavailable: 'スライサーの既定値を表示しています。選択したプリセットの値を読み取れませんでした。変更しない項目は引き続きプリセットの値が使われます。',
     filamentDefault: '既定',
     fromFile: 'ファイル由来',

+ 3 - 0
frontend/src/i18n/locales/ko.ts

@@ -4070,6 +4070,9 @@ export default {
     },
   },
   slicerSettings: {
+    presetValuesOutdatedSidecar: '슬라이서 기본값을 표시합니다. 슬라이서 사이드카가 이 기능보다 오래되어 프리셋 값을 가져올 수 없습니다. 값을 보려면 사이드카 이미지를 업데이트하세요. 변경하지 않은 항목은 계속 프리셋 값을 사용합니다.',
+    presetValuesNotConfigured: '슬라이서 기본값을 표시합니다. 슬라이서 사이드카가 설정되지 않아 프리셋 값을 읽을 수 없습니다. 변경하지 않은 항목은 계속 프리셋 값을 사용합니다.',
+    presetValuesSidecarUnavailable: '슬라이서 기본값을 표시합니다. 슬라이서 사이드카가 응답하지 않아 프리셋 값을 읽을 수 없습니다. 변경하지 않은 항목은 계속 프리셋 값을 사용합니다.',
     presetValuesUnavailable: '슬라이서 기본값을 표시합니다. 선택한 프리셋의 값을 읽을 수 없었습니다. 변경하지 않은 항목은 계속 프리셋 값을 사용합니다.',
     filamentDefault: '기본값',
     fromFile: '파일에서',

+ 3 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -4267,6 +4267,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Exibindo os padrões do fatiador: seu sidecar é mais antigo que este recurso e não consegue informar os valores de um perfil. Atualize a imagem do sidecar para vê-los. Tudo o que você não alterar continua usando o perfil.',
+    presetValuesNotConfigured: 'Exibindo os padrões do fatiador: nenhum sidecar está configurado, portanto os valores de um perfil não podem ser lidos. Tudo o que você não alterar continua usando o perfil.',
+    presetValuesSidecarUnavailable: 'Exibindo os padrões do fatiador: o sidecar não respondeu, portanto os valores de um perfil não podem ser lidos. Tudo o que você não alterar continua usando o perfil.',
     presetValuesUnavailable: 'Exibindo os padrões do fatiador: não foi possível ler os valores do perfil escolhido. Tudo o que você não alterar continua usando o perfil.',
     filamentDefault: 'Padrão',
     fromFile: 'do arquivo',

+ 3 - 0
frontend/src/i18n/locales/ru.ts

@@ -4062,6 +4062,9 @@ export default {
     },
   },
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Показаны значения по умолчанию слайсера: ваш sidecar старше этой функции и не может сообщить значения профиля. Обновите образ sidecar, чтобы увидеть их. Всё, что вы не измените, по-прежнему берётся из профиля.',
+    presetValuesNotConfigured: 'Показаны значения по умолчанию слайсера: sidecar не настроен, поэтому значения профиля прочитать нельзя. Всё, что вы не измените, по-прежнему берётся из профиля.',
+    presetValuesSidecarUnavailable: 'Показаны значения по умолчанию слайсера: sidecar не ответил, поэтому значения профиля прочитать нельзя. Всё, что вы не измените, по-прежнему берётся из профиля.',
     presetValuesUnavailable: 'Показаны значения по умолчанию слайсера: значения выбранного профиля прочитать не удалось. Всё, что вы не измените, по-прежнему берётся из профиля.',
     filamentDefault: 'По умолчанию',
     fromFile: 'из файла',

+ 3 - 0
frontend/src/i18n/locales/tr.ts

@@ -4268,6 +4268,9 @@ export default {
 
   // Dilimle (SliceModal ile slicer-API entegrasyonu)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Dilimleyici varsayılanları gösteriliyor: dilimleyici sidecar bu özellikten eski olduğu için bir ön ayarın değerlerini bildiremiyor. Görmek için sidecar imajını güncelleyin. Değiştirmediğiniz her şey yine ön ayarı kullanır.',
+    presetValuesNotConfigured: 'Dilimleyici varsayılanları gösteriliyor: yapılandırılmış bir sidecar olmadığı için ön ayarın değerleri okunamıyor. Değiştirmediğiniz her şey yine ön ayarı kullanır.',
+    presetValuesSidecarUnavailable: 'Dilimleyici varsayılanları gösteriliyor: sidecar yanıt vermediği için ön ayarın değerleri okunamıyor. Değiştirmediğiniz her şey yine ön ayarı kullanır.',
     presetValuesUnavailable: 'Dilimleyici varsayılanları gösteriliyor: seçilen ön ayarın kendi değerleri okunamadı. Değiştirmediğiniz her şey yine ön ayarı kullanır.',
     filamentDefault: 'Varsayılan',
     fromFile: 'dosyadan',

+ 3 - 0
frontend/src/i18n/locales/uk.ts

@@ -4312,6 +4312,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: 'Показано типові значення слайсера: ваш sidecar старіший за цю функцію і не може повідомити значення профілю. Оновіть образ sidecar, щоб їх побачити. Усе, чого ви не змінюєте, і далі береться з профілю.',
+    presetValuesNotConfigured: 'Показано типові значення слайсера: sidecar не налаштовано, тому значення профілю прочитати не можна. Усе, чого ви не змінюєте, і далі береться з профілю.',
+    presetValuesSidecarUnavailable: 'Показано типові значення слайсера: sidecar не відповів, тому значення профілю прочитати не можна. Усе, чого ви не змінюєте, і далі береться з профілю.',
     presetValuesUnavailable: 'Показано типові значення слайсера: значення вибраного профілю не вдалося прочитати. Усе, чого ви не змінюєте, і далі береться з профілю.',
     filamentDefault: 'За замовчуванням',
     fromFile: 'з файлу',

+ 3 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -4267,6 +4267,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: '当前显示切片器默认值:切片 sidecar 版本早于此功能,无法提供预设的实际值。更新 sidecar 镜像即可查看。未改动的项目仍使用预设。',
+    presetValuesNotConfigured: '当前显示切片器默认值:未配置切片 sidecar,无法读取预设的实际值。未改动的项目仍使用预设。',
+    presetValuesSidecarUnavailable: '当前显示切片器默认值:切片 sidecar 未响应,无法读取预设的实际值。未改动的项目仍使用预设。',
     presetValuesUnavailable: '当前显示切片器默认值:无法读取所选预设的实际值。未改动的项目仍使用预设。',
     filamentDefault: '默认',
     fromFile: '来自文件',

+ 3 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -4267,6 +4267,9 @@ export default {
 
   // Slice (slicer-API integration via SliceModal)
   slicerSettings: {
+    presetValuesOutdatedSidecar: '目前顯示切片器預設值:切片 sidecar 版本早於此功能,無法提供預設的實際值。更新 sidecar 映像即可查看。未變更的項目仍使用預設。',
+    presetValuesNotConfigured: '目前顯示切片器預設值:未設定切片 sidecar,無法讀取預設的實際值。未變更的項目仍使用預設。',
+    presetValuesSidecarUnavailable: '目前顯示切片器預設值:切片 sidecar 未回應,無法讀取預設的實際值。未變更的項目仍使用預設。',
     presetValuesUnavailable: '目前顯示切片器預設值:無法讀取所選預設的實際值。未變更的項目仍使用預設。',
     filamentDefault: '預設',
     fromFile: '來自檔案',

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/index-D1NzqW4U.js


+ 1 - 1
static/index.html

@@ -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-i4ueH5ac.js"></script>
+    <script type="module" crossorigin src="/assets/index-D1NzqW4U.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-D4VkH83v.css">
   </head>
   <body>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini