فهرست منبع

fix(users): admins can remove a locked-out user's 2FA from Edit user

The wiki described it, but Edit user had no control and the client
helper sent no body, so the admin password the route requires never
reached it. Edit user now has Disable 2FA (admins only, not on your own
account) asking for the admin's own password; LDAP and SSO admins are
asked only when the route says their account has one. The route reports
whether the user had any 2FA to remove.
maziggy 5 روز پیش
والد
کامیت
9fe528d9a3
100فایلهای تغییر یافته به همراه464 افزوده شده و 22 حذف شده
  1. 1 0
      CHANGELOG.md
  2. 5 1
      backend/app/api/routes/mfa.py
  3. 44 0
      backend/tests/integration/test_mfa_api.py
  4. 167 0
      frontend/src/__tests__/components/AdminDisableTwoFactor.test.tsx
  5. 7 2
      frontend/src/api/client.ts
  6. 111 0
      frontend/src/components/AdminDisableTwoFactor.tsx
  7. 7 0
      frontend/src/i18n/locales/de.ts
  8. 7 0
      frontend/src/i18n/locales/en.ts
  9. 7 0
      frontend/src/i18n/locales/es.ts
  10. 7 0
      frontend/src/i18n/locales/fr.ts
  11. 7 0
      frontend/src/i18n/locales/it.ts
  12. 7 0
      frontend/src/i18n/locales/ja.ts
  13. 8 1
      frontend/src/i18n/locales/ko.ts
  14. 7 0
      frontend/src/i18n/locales/nl.ts
  15. 7 0
      frontend/src/i18n/locales/pt-BR.ts
  16. 7 0
      frontend/src/i18n/locales/ru.ts
  17. 7 0
      frontend/src/i18n/locales/sv.ts
  18. 7 0
      frontend/src/i18n/locales/tr.ts
  19. 7 0
      frontend/src/i18n/locales/uk.ts
  20. 7 0
      frontend/src/i18n/locales/zh-CN.ts
  21. 7 0
      frontend/src/i18n/locales/zh-TW.ts
  22. 7 0
      frontend/src/pages/SettingsPage.tsx
  23. 0 0
      static/assets/AdditionalSection-D19VtSXK.js
  24. 0 0
      static/assets/AmsUnitCard-DUVGmorU.js
  25. 0 0
      static/assets/ArchivesPage-CrRBSiiu.js
  26. 0 0
      static/assets/AreaChart-CNUYgfu2.js
  27. 0 0
      static/assets/AssignToAmsModal-BSgNbELG.js
  28. 0 0
      static/assets/AuthContext-BQuxe7K9.js
  29. 1 1
      static/assets/Button-BXOnmmh7.js
  30. 0 0
      static/assets/CamWallPage-Bsaem5Q5.js
  31. 0 0
      static/assets/CameraDiagnoseModal-ORoWYJrl.js
  32. 0 0
      static/assets/CameraPage-CUs9LkoT.js
  33. 0 0
      static/assets/CameraWall-AZmzq7EY.js
  34. 1 1
      static/assets/Card-PxwWH-ub.js
  35. 0 0
      static/assets/CartesianChart-DyXiX83C.js
  36. 0 0
      static/assets/ColumnConfigModal-BWd2YJW7.js
  37. 0 0
      static/assets/ConfigureAmsSlotModal-ByQX-C31.js
  38. 0 0
      static/assets/ConnectAuthorizePage-Cqn2eQU3.js
  39. 0 0
      static/assets/ContextMenu-CRsw4iUt.js
  40. 0 0
      static/assets/CopyButton-D37Ln8vd.js
  41. 0 0
      static/assets/ErrorBarContext-BXvaZPsx.js
  42. 0 0
      static/assets/ExternalLinkPage-DfmwdIWm.js
  43. 0 1
      static/assets/FileManagerPage-B-PQF-jW.js
  44. 0 0
      static/assets/FileUploadModal-EXrQ0AVn.js
  45. 0 0
      static/assets/FinancePage-CyfUzRt9.js
  46. 0 0
      static/assets/GCodeViewerPage-B-n3uOHw.js
  47. 0 0
      static/assets/GcodeToolpathViewer-BvTY9h_R.js
  48. 0 0
      static/assets/GroupEditPage-IHdiAnyS.js
  49. 0 0
      static/assets/ImagePreviewModal-DtQrNGik.js
  50. 0 0
      static/assets/InventoryPage-DLVRmNbY.js
  51. 0 0
      static/assets/LibraryTrashPage-BfmIe8XZ.js
  52. 0 0
      static/assets/LoginPage-BurzdSat.js
  53. 0 0
      static/assets/MaintenancePage-lKPEJczF.js
  54. 0 0
      static/assets/ModelSourcesPage-8KjTi7f-.js
  55. 0 0
      static/assets/NotificationsPage-2xsgTLgp.js
  56. 1 1
      static/assets/OverlayFrame-C5o8QwmR.js
  57. 0 1
      static/assets/PdfPreviewModal-DMiJMUO5.js
  58. 0 0
      static/assets/PhotoGalleryModal-pzgeQD_Y.js
  59. 0 0
      static/assets/PrinterLocationsPage-BR0B7TF0.js
  60. 0 0
      static/assets/PrintersPage-CxQMHxc6.js
  61. 0 0
      static/assets/ProfilesPage-D1H_BJ94.js
  62. 0 0
      static/assets/ProjectDetailPage-Dm3fpK73.js
  63. 0 0
      static/assets/ProjectsPage-BvIE1Umi.js
  64. 0 0
      static/assets/QueryClientProvider-DNxj-7q3.js
  65. 0 0
      static/assets/QueuePage-DcWQgG7d.js
  66. 0 0
      static/assets/ReferenceLine-yMeXz9U9.js
  67. 0 0
      static/assets/RichTextEditor-D-_UM6sz.js
  68. 0 0
      static/assets/SettingsPage-CymzMUkk.js
  69. 0 0
      static/assets/SettingsPage-DAd2Rbkn.js
  70. 0 0
      static/assets/SetupPage-Coue7Gnb.js
  71. 0 0
      static/assets/SliceModal-CH7h2Sgw.js
  72. 0 0
      static/assets/SpoolBuddyAmsPage-BctrcVtm.js
  73. 0 0
      static/assets/SpoolBuddyCalibrationPage-u7BfdSCv.js
  74. 0 0
      static/assets/SpoolBuddyDashboard-D73GYLxE.js
  75. 0 0
      static/assets/SpoolBuddyInventoryPage-D30YmeEZ.js
  76. 1 1
      static/assets/SpoolBuddySettingsPage-CLU7n5Tj.js
  77. 0 0
      static/assets/SpoolBuddyWriteTagPage-CbXN_BXQ.js
  78. 1 1
      static/assets/SpoolIcon-Ckk9N7-6.js
  79. 0 0
      static/assets/SpreadsheetPreviewModal-DvAy1X-M.js
  80. 0 0
      static/assets/StatsPage-Qip2cyZj.js
  81. 0 0
      static/assets/StreamOverlayPage-CyHO7ILh.js
  82. 0 0
      static/assets/SystemInfoPage-CpJNOXPh.js
  83. 0 0
      static/assets/ToastContext-Bhj9DS2p.js
  84. 1 1
      static/assets/Toggle-M-zinw5Z.js
  85. 0 1
      static/assets/arrow-down-Cxl6vzXB.js
  86. 1 0
      static/assets/arrow-down-VmQok7Hw.js
  87. 1 0
      static/assets/arrow-left-BcrTUh-S.js
  88. 0 1
      static/assets/arrow-left-BdDYciL5.js
  89. 0 1
      static/assets/arrow-right-D8L4bue0.js
  90. 1 0
      static/assets/arrow-right-bfq0Qx0q.js
  91. 0 1
      static/assets/arrow-up-BA-zWepm.js
  92. 1 0
      static/assets/arrow-up-DXtx7YeZ.js
  93. 1 1
      static/assets/arrow-up-down-DUGhy30I.js
  94. 1 1
      static/assets/box-C9odJhAD.js
  95. 1 1
      static/assets/briefcase--0-VcGda.js
  96. 1 1
      static/assets/calendar-C_bF78FG.js
  97. 1 0
      static/assets/check-DLgNJCRi.js
  98. 0 1
      static/assets/check-DeJLbG-z.js
  99. 1 1
      static/assets/chevrons-right-CEkMMhQF.js
  100. 0 0
      static/assets/client-1U8CP3-A.js

+ 1 - 0
CHANGELOG.md

@@ -164,6 +164,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **Admins can remove a locked-out user's 2FA from Edit user** — The wiki described this, but Edit user had no control for it, so a user who lost their authenticator stayed locked out unless an admin called the API by hand. **Settings → Users → Edit user** now has **Disable 2FA**, which asks for the admin's own password (admins who sign in through LDAP or SSO usually have none and are not asked) and says whether the user had any 2FA to remove.
 - **The AMS drying button knows a printer cannot dry from the first page load** — After a page load and every 30-second refresh, the printer card didn't know the drying status or why drying wasn't possible, so a printer that cannot dry showed an enabled Start button until the next live update (the printer still refused the start). The page load and the live updates now describe AMS units and external spools in exactly the same way. A slot the firmware reports as only its number now also counts as empty from the first load, and the AMS temperature no longer switches between "24°C" and "24.0°C".
 - **The filament override picker keeps both external spools of a dual-nozzle printer without an AMS apart** — On an H2D or X2D feeding only from its external spools, identical spools on the left and right merged into one entry that was offered for both nozzles. The picker now uses the same dual-nozzle check as the print scheduler, so each external spool is offered only for the nozzle it feeds.
 - **A2L: filament used from the AMS Lite is charged to the right spool** — The printer names an AMS Lite tray by its local slot 1-4, and Bambuddy kept that number when it recorded which trays a print used, so the AMS Lite spool was never charged. With a regular AMS beside the AMS Lite, the usage went to that AMS's slot of the same number instead. A virtual printer with **Save AMS mapping** on stored the same local number for queued prints and their archives; with a regular AMS present, such a print was sent to that AMS instead of the AMS Lite.

+ 5 - 1
backend/app/api/routes/mfa.py

@@ -1301,6 +1301,10 @@ async def admin_disable_2fa(
     if target_user is None:
         raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
 
+    # Reported back so the Edit user dialog can say whether anything was removed.
+    totp = (await db.execute(select(UserTOTP).where(UserTOTP.user_id == user_id))).scalar_one_or_none()
+    had_2fa = bool(totp is not None and totp.is_enabled) or await _get_email_2fa_enabled(db, user_id)
+
     # Delete TOTP record
     await db.execute(delete(UserTOTP).where(UserTOTP.user_id == user_id))
 
@@ -1330,7 +1334,7 @@ async def admin_disable_2fa(
     await ws_manager.close_user(user_id)
     actor = current_user.username if current_user else "anonymous"
     logger.info("Admin %s disabled all 2FA for user_id=%d", actor, user_id)
-    return {"message": "2FA disabled for user"}
+    return {"message": "2FA disabled for user", "had_2fa": had_2fa}
 
 
 # ===========================================================================

+ 44 - 0
backend/tests/integration/test_mfa_api.py

@@ -652,6 +652,49 @@ class TestRateLimiting:
 class TestAdminDisable2FA:
     """Tests for DELETE /api/v1/auth/2fa/admin/{user_id}."""
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_admin_disable_reports_a_user_without_2fa(self, async_client: AsyncClient):
+        """The Edit user dialog says so, rather than claiming it removed something."""
+        token = await _setup_and_login(async_client, "adminno2fa", "adminno2fa123")
+        created = await async_client.post(
+            "/api/v1/users",
+            json={"username": "plainuser2fa", "password": "Plainuser2fa1!"},
+            headers=_auth_header(token),
+        )
+        response = await async_client.request(
+            "DELETE",
+            f"/api/v1/auth/2fa/admin/{created.json()['id']}",
+            json={"admin_password": _norm_pw("adminno2fa123")},
+            headers=_auth_header(token),
+        )
+        assert response.status_code == 200
+        assert response.json()["had_2fa"] is False
+
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_admin_disable_reports_email_2fa(self, async_client: AsyncClient, db_session):
+        from backend.app.api.routes.settings import set_setting
+
+        token = await _setup_and_login(async_client, "adminmail2fa", "adminmail2fa123")
+        created = await async_client.post(
+            "/api/v1/users",
+            json={"username": "mailuser2fa", "password": "Mailuser2fa1!"},
+            headers=_auth_header(token),
+        )
+        user_id = created.json()["id"]
+        await set_setting(db_session, f"user_{user_id}_email_2fa_enabled", "true")
+        await db_session.commit()
+
+        response = await async_client.request(
+            "DELETE",
+            f"/api/v1/auth/2fa/admin/{user_id}",
+            json={"admin_password": _norm_pw("adminmail2fa123")},
+            headers=_auth_header(token),
+        )
+        assert response.status_code == 200
+        assert response.json()["had_2fa"] is True
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_admin_disable_unknown_user_is_404(self, async_client: AsyncClient):
@@ -692,6 +735,7 @@ class TestAdminDisable2FA:
             headers=_auth_header(token),
         )
         assert response.status_code == 200
+        assert response.json()["had_2fa"] is True
 
         # I2: admin_disable_2fa bumps password_changed_at, invalidating the old token.
         # Re-login to get a fresh token before checking status.

+ 167 - 0
frontend/src/__tests__/components/AdminDisableTwoFactor.test.tsx

@@ -0,0 +1,167 @@
+/**
+ * Admin "Disable 2FA" in the Edit user dialog.
+ *
+ * The wiki has long told admins to remove a locked-out user's 2FA from Edit
+ * user, but there was no control for it, and the client helper sent no body,
+ * so the admin password the route requires never reached it.
+ */
+
+import { http, HttpResponse } from 'msw';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import userEvent from '@testing-library/user-event';
+
+import { render, screen, waitFor } from '../utils';
+import { server } from '../mocks/server';
+
+const mockShowToast = vi.fn();
+const mockAuth = {
+  user: { id: 1, username: 'admin', auth_source: 'local' } as { id: number; username: string; auth_source: string },
+  authEnabled: true,
+  isAdmin: true,
+};
+
+vi.mock('../../contexts/AuthContext', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
+  return {
+    ...actual,
+    useAuth: () => ({
+      ...mockAuth,
+      requiresSetup: false,
+      loading: false,
+      hasPermission: () => true,
+      hasAnyPermission: () => true,
+      hasAllPermissions: () => true,
+      canModify: () => true,
+      login: vi.fn(),
+      loginWithToken: vi.fn(),
+      logout: vi.fn(),
+      refreshUser: vi.fn(),
+      refreshAuth: vi.fn(),
+    }),
+  };
+});
+
+vi.mock('../../contexts/ToastContext', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
+  return { ...actual, useToast: () => ({ showToast: mockShowToast }) };
+});
+
+import { AdminDisableTwoFactor } from '../../components/AdminDisableTwoFactor';
+
+function captureDelete(response: object = { message: 'ok', had_2fa: true }, status = 200) {
+  const calls: { id: string; body: unknown }[] = [];
+  server.use(
+    http.delete('/api/v1/auth/2fa/admin/:id', async ({ request, params }) => {
+      calls.push({ id: String(params.id), body: await request.json() });
+      return HttpResponse.json(response, { status });
+    }),
+  );
+  return calls;
+}
+
+describe('AdminDisableTwoFactor', () => {
+  beforeEach(() => {
+    mockShowToast.mockReset();
+    mockAuth.user = { id: 1, username: 'admin', auth_source: 'local' };
+    mockAuth.isAdmin = true;
+    mockAuth.authEnabled = true;
+  });
+
+  it('sends the admin password and says the 2FA was removed', async () => {
+    const calls = captureDelete();
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+    await userEvent.type(screen.getByLabelText('Your password'), 'admin-pass');
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+
+    await waitFor(() => expect(calls).toHaveLength(1));
+    expect(calls[0]).toEqual({ id: '7', body: { admin_password: 'admin-pass' } });
+    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Two-factor authentication removed for bob', 'success'));
+  });
+
+  it('says so when the user had no 2FA', async () => {
+    captureDelete({ message: 'ok', had_2fa: false });
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+    await userEvent.type(screen.getByLabelText('Your password'), 'admin-pass');
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+
+    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('bob had no two-factor authentication to remove', 'info'));
+  });
+
+  it('cannot confirm without a password', async () => {
+    const calls = captureDelete();
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+
+    expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled();
+    expect(calls).toHaveLength(0);
+  });
+
+  it('asks no password of an admin without a local one', async () => {
+    mockAuth.user = { id: 1, username: 'sso-admin', auth_source: 'oidc' };
+    const calls = captureDelete();
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+    expect(screen.queryByLabelText('Your password')).not.toBeInTheDocument();
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+
+    await waitFor(() => expect(calls).toHaveLength(1));
+    expect(calls[0].body).toEqual({ admin_password: null });
+  });
+
+  it('asks for the password when an SSO admin turns out to have one', async () => {
+    // An OIDC account can be given a password in Edit user; the route then asks for it.
+    mockAuth.user = { id: 1, username: 'sso-admin', auth_source: 'oidc' };
+    const calls: unknown[] = [];
+    server.use(
+      http.delete('/api/v1/auth/2fa/admin/:id', async ({ request }) => {
+        const body = (await request.json()) as { admin_password: string | null };
+        calls.push(body);
+        if (!body.admin_password) return HttpResponse.json({ detail: 'Admin password required' }, { status: 401 });
+        return HttpResponse.json({ message: 'ok', had_2fa: true });
+      }),
+    );
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+    await userEvent.type(await screen.findByLabelText('Your password'), 'sso-local-pass');
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+
+    await waitFor(() => expect(calls).toEqual([{ admin_password: null }, { admin_password: 'sso-local-pass' }]));
+    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith('Two-factor authentication removed for bob', 'success'));
+  });
+
+  it('shows the server refusal', async () => {
+    captureDelete({ detail: 'Admin password required' }, 401);
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+
+    await userEvent.click(screen.getByRole('button', { name: 'Disable 2FA' }));
+    await userEvent.type(screen.getByLabelText('Your password'), 'wrong');
+    await userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
+
+    await waitFor(() => expect(mockShowToast).toHaveBeenCalledWith(expect.stringContaining('Admin password required'), 'error'));
+  });
+
+  it('is not offered to a non-admin', () => {
+    mockAuth.isAdmin = false;
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+    expect(screen.queryByRole('button', { name: 'Disable 2FA' })).not.toBeInTheDocument();
+  });
+
+  it('is not offered on your own account, which has its own 2FA settings', () => {
+    render(<AdminDisableTwoFactor userId={1} username="admin" />);
+    expect(screen.queryByRole('button', { name: 'Disable 2FA' })).not.toBeInTheDocument();
+  });
+
+  it('is not offered with authentication off', () => {
+    mockAuth.authEnabled = false;
+    render(<AdminDisableTwoFactor userId={7} username="bob" />);
+    expect(screen.queryByRole('button', { name: 'Disable 2FA' })).not.toBeInTheDocument();
+  });
+});

+ 7 - 2
frontend/src/api/client.ts

@@ -4963,8 +4963,13 @@ export const api = {
     }),
 
   // 2FA - admin
-  admin2FADisable: (userId: number) =>
-    request<{ message: string }>(`/auth/2fa/admin/${userId}`, { method: 'DELETE' }),
+  // adminPassword: the calling admin's own password; null for an admin
+  // without one (LDAP / OIDC accounts usually), whom the route does not ask.
+  admin2FADisable: (userId: number, adminPassword: string | null) =>
+    request<{ message: string; had_2fa: boolean }>(`/auth/2fa/admin/${userId}`, {
+      method: 'DELETE',
+      body: JSON.stringify({ admin_password: adminPassword }),
+    }),
 
   // OIDC providers (public list)
   getOIDCProviders: () => request<OIDCProviderPublic[]>('/auth/oidc/providers'),

+ 111 - 0
frontend/src/components/AdminDisableTwoFactor.tsx

@@ -0,0 +1,111 @@
+import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { ShieldOff } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+import { api, ApiError } from '../api/client';
+import { Button } from './Button';
+import { useToast } from '../contexts/ToastContext';
+import { useAuth } from '../contexts/AuthContext';
+
+interface AdminDisableTwoFactorProps {
+  userId: number;
+  username: string;
+}
+
+/**
+ * Edit user: remove a user's 2FA (TOTP, email codes and backup codes), for a
+ * user locked out of their authenticator. Admin-only, like the route. Not shown
+ * on your own account, which has its own 2FA settings. The route asks for the
+ * admin's own password unless the admin has none (LDAP and OIDC accounts,
+ * unless they were given one).
+ */
+export function AdminDisableTwoFactor({ userId, username }: AdminDisableTwoFactorProps) {
+  const { t } = useTranslation();
+  const { showToast } = useToast();
+  const { authEnabled, isAdmin, user } = useAuth();
+  const [confirming, setConfirming] = useState(false);
+  const [password, setPassword] = useState('');
+  // Set when the route asks for a password the sign-in type did not suggest:
+  // an LDAP or SSO account that was given a local password in Edit user.
+  const [passwordAsked, setPasswordAsked] = useState(false);
+
+  const needsPassword = passwordAsked || user?.auth_source === 'local';
+
+  const disableMutation = useMutation({
+    mutationFn: () => api.admin2FADisable(userId, needsPassword ? password : null),
+    onSuccess: (response) => {
+      setConfirming(false);
+      setPassword('');
+      if (response.had_2fa) {
+        showToast(t('users.form.twoFaRemoved', { username }), 'success');
+      } else {
+        showToast(t('users.form.twoFaNoneToRemove', { username }), 'info');
+      }
+    },
+    onError: (error: Error) => {
+      if (!needsPassword && error instanceof ApiError && error.status === 401) {
+        setPasswordAsked(true);
+        showToast(t('users.form.twoFaPasswordNeeded'), 'info');
+        return;
+      }
+      showToast(error.message, 'error');
+    },
+  });
+
+  if (!authEnabled || !isAdmin || user?.id === userId) return null;
+
+  return (
+    <div className="bg-bambu-dark-secondary/50 border border-bambu-dark-tertiary rounded-lg p-3 space-y-3">
+      <div>
+        <p className="text-sm font-medium text-white">{t('users.form.twoFaTitle')}</p>
+        <p className="text-sm text-bambu-gray">{t('users.form.twoFaHint')}</p>
+      </div>
+      {confirming ? (
+        <div className="space-y-3">
+          {needsPassword && (
+            <div>
+              <label htmlFor="admin-disable-2fa-password" className="block text-sm font-medium text-white mb-1">
+                {t('users.form.twoFaYourPassword')}
+              </label>
+              <input
+                id="admin-disable-2fa-password"
+                type="password"
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                autoComplete="current-password"
+                className="w-full px-4 py-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green"
+              />
+            </div>
+          )}
+          <div className="flex gap-3">
+            <Button
+              variant="secondary"
+              size="sm"
+              className="flex-1"
+              onClick={() => {
+                setConfirming(false);
+                setPassword('');
+              }}
+            >
+              {t('common.cancel')}
+            </Button>
+            <Button
+              variant="danger"
+              size="sm"
+              className="flex-1"
+              disabled={disableMutation.isPending || (needsPassword && !password)}
+              onClick={() => disableMutation.mutate()}
+            >
+              {t('common.confirm')}
+            </Button>
+          </div>
+        </div>
+      ) : (
+        <Button variant="secondary" size="sm" className="w-full" onClick={() => setConfirming(true)}>
+          <ShieldOff className="w-4 h-4" />
+          {t('users.form.twoFaDisable')}
+        </Button>
+      )}
+    </div>
+  );
+}

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

@@ -3775,6 +3775,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Das Passwort wird durch erweiterte Authentifizierung verwaltet. Verwenden Sie "Passwort zurücksetzen", um dem Benutzer per E-Mail einen Link zum Festlegen eines neuen Passworts zu senden.',
       resetPassword: 'Passwort zurücksetzen',
       resettingPassword: 'Passwort wird zurückgesetzt...',
+      twoFaTitle: 'Zwei-Faktor-Authentifizierung',
+      twoFaHint: 'Für Benutzer, die keinen Zugriff mehr auf ihren Authenticator haben: entfernt Authenticator-App, E-Mail-Codes und Backup-Codes und meldet den Benutzer überall ab. Nach der Anmeldung kann 2FA neu eingerichtet werden.',
+      twoFaDisable: '2FA deaktivieren',
+      twoFaYourPassword: 'Ihr Passwort',
+      twoFaRemoved: 'Zwei-Faktor-Authentifizierung für {{username}} entfernt',
+      twoFaNoneToRemove: '{{username}} hatte keine Zwei-Faktor-Authentifizierung',
+      twoFaPasswordNeeded: 'Ihr Konto hat ein Passwort. Geben Sie es zur Bestätigung ein.',
     },
     deleteModal: {
       title: 'Benutzer löschen',

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

@@ -3807,6 +3807,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Password is managed by Advanced Authentication. Use "Reset Password" to email the user a link to set a new password.',
       resetPassword: 'Reset Password',
       resettingPassword: 'Resetting Password...',
+      twoFaTitle: 'Two-factor authentication',
+      twoFaHint: 'For a user locked out of their authenticator: removes their authenticator app, email codes and backup codes, and signs them out everywhere. They can set 2FA up again after signing in.',
+      twoFaDisable: 'Disable 2FA',
+      twoFaYourPassword: 'Your password',
+      twoFaRemoved: 'Two-factor authentication removed for {{username}}',
+      twoFaNoneToRemove: '{{username}} had no two-factor authentication to remove',
+      twoFaPasswordNeeded: 'Your account has a password. Enter it to confirm.',
     },
     deleteModal: {
       title: 'Delete User',

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

@@ -3777,6 +3777,13 @@ export default {
       passwordManagedByAdvancedAuth: 'La contraseña la gestiona la autenticación avanzada. Use «Restablecer contraseña» para enviar al usuario por correo un enlace para establecer una contraseña nueva.',
       resetPassword: 'Restablecer contraseña',
       resettingPassword: 'Restableciendo la contraseña...',
+      twoFaTitle: 'Autenticación de dos factores',
+      twoFaHint: 'Para un usuario sin acceso a su autenticador: elimina su app de autenticación, los códigos por correo y los códigos de respaldo, y cierra todas sus sesiones. Podrá volver a configurar 2FA al iniciar sesión.',
+      twoFaDisable: 'Desactivar 2FA',
+      twoFaYourPassword: 'Su contraseña',
+      twoFaRemoved: 'Autenticación de dos factores eliminada para {{username}}',
+      twoFaNoneToRemove: '{{username}} no tenía autenticación de dos factores',
+      twoFaPasswordNeeded: 'Su cuenta tiene una contraseña. Introdúzcala para confirmar.',
     },
     deleteModal: {
       title: 'Eliminar usuario',

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

@@ -3763,6 +3763,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Géré par Auth Avancée. Utilisez "Réinitialiser" pour envoyer par email un lien permettant de définir un nouveau mdp.',
       resetPassword: 'Réinitialiser le mot de passe',
       resettingPassword: 'Réinitialisation...',
+      twoFaTitle: 'Authentification à deux facteurs',
+      twoFaHint: 'Pour un utilisateur qui n\'a plus accès à son authentificateur : supprime son application d\'authentification, ses codes par e-mail et ses codes de secours, et le déconnecte partout. Il pourra reconfigurer la 2FA après s\'être connecté.',
+      twoFaDisable: 'Désactiver la 2FA',
+      twoFaYourPassword: 'Votre mot de passe',
+      twoFaRemoved: 'Authentification à deux facteurs supprimée pour {{username}}',
+      twoFaNoneToRemove: '{{username}} n\'avait pas d\'authentification à deux facteurs',
+      twoFaPasswordNeeded: 'Votre compte a un mot de passe. Saisissez-le pour confirmer.',
     },
     deleteModal: {
       title: 'Supprimer utilisateur',

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

@@ -3762,6 +3762,13 @@ export default {
       passwordManagedByAdvancedAuth: 'La password è gestita dall\'autenticazione avanzata. Usa "Reimposta password" per inviare all\'utente via email un link per impostare una nuova password.',
       resetPassword: 'Reimposta password',
       resettingPassword: 'Reimpostazione password...',
+      twoFaTitle: 'Autenticazione a due fattori',
+      twoFaHint: 'Per un utente che non ha più accesso al proprio autenticatore: rimuove l\'app di autenticazione, i codici via e-mail e i codici di backup, e lo disconnette ovunque. Potrà configurare di nuovo la 2FA dopo l\'accesso.',
+      twoFaDisable: 'Disattiva 2FA',
+      twoFaYourPassword: 'La tua password',
+      twoFaRemoved: 'Autenticazione a due fattori rimossa per {{username}}',
+      twoFaNoneToRemove: '{{username}} non aveva l\'autenticazione a due fattori',
+      twoFaPasswordNeeded: 'Il tuo account ha una password. Inseriscila per confermare.',
     },
     deleteModal: {
       title: 'Elimina utente',

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

@@ -3775,6 +3775,13 @@ export default {
       passwordManagedByAdvancedAuth: 'パスワードは高度な認証によって管理されています。「パスワードのリセット」を使用して、新しいパスワードを設定するためのリンクをメールでユーザーに送信してください。',
       resetPassword: 'パスワードのリセット',
       resettingPassword: 'パスワードをリセット中...',
+      twoFaTitle: '2段階認証',
+      twoFaHint: '認証アプリを使えなくなったユーザー向け:認証アプリ、メールコード、バックアップコードを削除し、すべてのセッションからサインアウトさせます。サインイン後に2段階認証を再設定できます。',
+      twoFaDisable: '2FAを無効化',
+      twoFaYourPassword: 'あなたのパスワード',
+      twoFaRemoved: '{{username}} の2段階認証を削除しました',
+      twoFaNoneToRemove: '{{username}} には削除する2段階認証がありませんでした',
+      twoFaPasswordNeeded: 'あなたのアカウントにはパスワードがあります。確認のため入力してください。',
     },
     deleteModal: {
       title: 'ユーザーを削除',

+ 8 - 1
frontend/src/i18n/locales/ko.ts

@@ -3601,7 +3601,14 @@ export default {
       autoGeneratedPassword: '안전한 비밀번호가 자동으로 생성되어 사용자에게 이메일로 전송됩니다.',
       passwordManagedByAdvancedAuth: '비밀번호는 고급 인증에 의해 관리됩니다. "비밀번호 재설정"을 사용하여 새 비밀번호를 설정할 수 있는 링크를 이메일로 전송하세요.',
       resetPassword: '비밀번호 재설정',
-      resettingPassword: '비밀번호 재설정 중...'
+      resettingPassword: '비밀번호 재설정 중...',
+      twoFaTitle: '2단계 인증',
+      twoFaHint: '인증 앱을 사용할 수 없게 된 사용자용: 인증 앱, 이메일 코드, 백업 코드를 제거하고 모든 곳에서 로그아웃시킵니다. 로그인 후 2단계 인증을 다시 설정할 수 있습니다.',
+      twoFaDisable: '2FA 비활성화',
+      twoFaYourPassword: '내 비밀번호',
+      twoFaRemoved: '{{username}}의 2단계 인증을 제거했습니다',
+      twoFaNoneToRemove: '{{username}}에게는 제거할 2단계 인증이 없습니다',
+      twoFaPasswordNeeded: '계정에 비밀번호가 있습니다. 확인하려면 입력하세요.'
     },
     deleteModal: {
       title: '사용자 삭제',

+ 7 - 0
frontend/src/i18n/locales/nl.ts

@@ -3805,6 +3805,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Wachtwoord wordt beheerd door Geavanceerde authenticatie. Gebruik "Wachtwoord resetten" om de gebruiker via e-mail een link te sturen om een nieuw wachtwoord in te stellen.',
       resetPassword: 'Wachtwoord resetten',
       resettingPassword: 'Wachtwoord resetten...',
+      twoFaTitle: 'Tweestapsverificatie',
+      twoFaHint: 'Voor een gebruiker die geen toegang meer heeft tot zijn authenticator: verwijdert de authenticator-app, e-mailcodes en back-upcodes, en meldt de gebruiker overal af. Na het inloggen kan 2FA opnieuw worden ingesteld.',
+      twoFaDisable: '2FA uitschakelen',
+      twoFaYourPassword: 'Je wachtwoord',
+      twoFaRemoved: 'Tweestapsverificatie verwijderd voor {{username}}',
+      twoFaNoneToRemove: '{{username}} had geen tweestapsverificatie',
+      twoFaPasswordNeeded: 'Je account heeft een wachtwoord. Voer het in om te bevestigen.',
     },
     deleteModal: {
       title: 'Gebruiker verwijderen',

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

@@ -3762,6 +3762,13 @@ export default {
       passwordManagedByAdvancedAuth: 'A senha é gerenciada pela Autenticação Avançada. Use "Redefinir Senha" para enviar ao usuário por e-mail um link para definir uma nova senha.',
       resetPassword: 'Redefinir Senha',
       resettingPassword: 'Redefinindo Senha...',
+      twoFaTitle: 'Autenticação de dois fatores',
+      twoFaHint: 'Para um usuário sem acesso ao autenticador: remove o app autenticador, os códigos por e-mail e os códigos de backup, e encerra todas as sessões. O usuário pode configurar o 2FA novamente após entrar.',
+      twoFaDisable: 'Desativar 2FA',
+      twoFaYourPassword: 'Sua senha',
+      twoFaRemoved: 'Autenticação de dois fatores removida para {{username}}',
+      twoFaNoneToRemove: '{{username}} não tinha autenticação de dois fatores',
+      twoFaPasswordNeeded: 'Sua conta tem uma senha. Digite-a para confirmar.',
     },
     deleteModal: {
       title: 'Excluir Usuário',

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

@@ -3594,6 +3594,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Пароль управляется расширенной аутентификацией. Используйте «Сбросить пароль», чтобы отправить пользователю по email ссылку для установки нового пароля.',
       resetPassword: "Сбросить пароль",
       resettingPassword: "Сброс пароля...",
+      twoFaTitle: "Двухфакторная аутентификация",
+      twoFaHint: "Для пользователя, потерявшего доступ к аутентификатору: удаляет приложение-аутентификатор, коды по e-mail и резервные коды и завершает все его сеансы. После входа 2FA можно настроить заново.",
+      twoFaDisable: "Отключить 2FA",
+      twoFaYourPassword: "Ваш пароль",
+      twoFaRemoved: "Двухфакторная аутентификация для {{username}} удалена",
+      twoFaNoneToRemove: "У {{username}} не было двухфакторной аутентификации",
+      twoFaPasswordNeeded: "У вашей учётной записи есть пароль. Введите его для подтверждения.",
     },
     deleteModal: {
       title: "Удаление пользователя",

+ 7 - 0
frontend/src/i18n/locales/sv.ts

@@ -3804,6 +3804,13 @@ errors: {
       passwordManagedByAdvancedAuth: 'Lösenordet hanteras av avancerad autentisering. Använd "Återställ lösenord" för att via e-post skicka användaren en länk för att ange ett nytt lösenord.',
       resetPassword: 'Återställ lösenord',
       resettingPassword: 'Återställer lösenord...',
+      twoFaTitle: 'Tvåfaktorsautentisering',
+      twoFaHint: 'För en användare som inte längre kommer åt sin autentiserare: tar bort autentiseringsappen, e-postkoder och reservkoder och loggar ut användaren överallt. 2FA kan ställas in igen efter inloggning.',
+      twoFaDisable: 'Inaktivera 2FA',
+      twoFaYourPassword: 'Ditt lösenord',
+      twoFaRemoved: 'Tvåfaktorsautentisering borttagen för {{username}}',
+      twoFaNoneToRemove: '{{username}} hade ingen tvåfaktorsautentisering',
+      twoFaPasswordNeeded: 'Ditt konto har ett lösenord. Ange det för att bekräfta.',
     },
     deleteModal: {
       title: 'Ta bort användare',

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

@@ -3778,6 +3778,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Parola Gelişmiş Kimlik Doğrulama tarafından yönetilir. Kullanıcıya e-posta yoluyla yeni bir parola belirleme bağlantısı göndermek için "Parolayı Sıfırla"yı kullanın.',
       resetPassword: 'Parolayı Sıfırla',
       resettingPassword: 'Parola Sıfırlanıyor...',
+      twoFaTitle: 'İki faktörlü kimlik doğrulama',
+      twoFaHint: 'Kimlik doğrulayıcısına erişimi kalmayan bir kullanıcı için: kimlik doğrulama uygulamasını, e-posta kodlarını ve yedek kodları kaldırır ve kullanıcının tüm oturumlarını kapatır. Giriş yaptıktan sonra 2FA yeniden kurulabilir.',
+      twoFaDisable: '2FA\'yı devre dışı bırak',
+      twoFaYourPassword: 'Parolanız',
+      twoFaRemoved: '{{username}} için iki faktörlü kimlik doğrulama kaldırıldı',
+      twoFaNoneToRemove: '{{username}} için kaldırılacak iki faktörlü kimlik doğrulama yoktu',
+      twoFaPasswordNeeded: 'Hesabınızın bir parolası var. Onaylamak için girin.',
     },
     deleteModal: {
       title: 'Kullanıcıyı Sil',

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

@@ -3802,6 +3802,13 @@ export default {
       passwordManagedByAdvancedAuth: 'Пароль керується розширеною автентифікацією. Використовуйте «Скинути пароль», щоб надіслати користувачеві електронною поштою посилання для встановлення нового пароля.',
       resetPassword: "Скинути пароль",
       resettingPassword: "Скидання пароля...",
+      twoFaTitle: "Двофакторна автентифікація",
+      twoFaHint: "Для користувача, який втратив доступ до автентифікатора: видаляє застосунок-автентифікатор, коди електронною поштою та резервні коди й завершує всі його сеанси. Після входу 2FA можна налаштувати знову.",
+      twoFaDisable: "Вимкнути 2FA",
+      twoFaYourPassword: "Ваш пароль",
+      twoFaRemoved: "Двофакторну автентифікацію для {{username}} видалено",
+      twoFaNoneToRemove: "У {{username}} не було двофакторної автентифікації",
+      twoFaPasswordNeeded: "Ваш обліковий запис має пароль. Введіть його для підтвердження.",
     },
     deleteModal: {
       title: "Видалити користувача",

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

@@ -3762,6 +3762,13 @@ export default {
       passwordManagedByAdvancedAuth: '密码由高级认证管理。使用"重置密码"通过邮件向用户发送设置新密码的链接。',
       resetPassword: '重置密码',
       resettingPassword: '重置密码中...',
+      twoFaTitle: '双重验证',
+      twoFaHint: '适用于无法使用验证器的用户:删除其验证器应用、邮件验证码和备用码,并使其在所有设备上退出登录。登录后可重新设置双重验证。',
+      twoFaDisable: '停用双重验证',
+      twoFaYourPassword: '您的密码',
+      twoFaRemoved: '已删除 {{username}} 的双重验证',
+      twoFaNoneToRemove: '{{username}} 没有可删除的双重验证',
+      twoFaPasswordNeeded: '您的账户设有密码。请输入密码以确认。',
     },
     deleteModal: {
       title: '删除用户',

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

@@ -3762,6 +3762,13 @@ export default {
       passwordManagedByAdvancedAuth: '密碼由進階認證管理。使用"重設密碼"透過郵件向使用者傳送設定新密碼的連結。',
       resetPassword: '重設密碼',
       resettingPassword: '重設密碼中...',
+      twoFaTitle: '雙重驗證',
+      twoFaHint: '適用於無法使用驗證器的使用者:刪除其驗證器應用程式、電子郵件驗證碼和備用碼,並使其在所有裝置上登出。登入後可重新設定雙重驗證。',
+      twoFaDisable: '停用雙重驗證',
+      twoFaYourPassword: '您的密碼',
+      twoFaRemoved: '已刪除 {{username}} 的雙重驗證',
+      twoFaNoneToRemove: '{{username}} 沒有可刪除的雙重驗證',
+      twoFaPasswordNeeded: '您的帳戶設有密碼。請輸入密碼以確認。',
     },
     deleteModal: {
       title: '刪除使用者',

+ 7 - 0
frontend/src/pages/SettingsPage.tsx

@@ -53,6 +53,7 @@ import { EmailSettings } from '../components/EmailSettings';
 import { LDAPSettings } from '../components/LDAPSettings';
 import { PrinterAccessSettings } from '../components/PrinterAccessSettings';
 import { TwoFactorSettings } from '../components/TwoFactorSettings';
+import { AdminDisableTwoFactor } from '../components/AdminDisableTwoFactor';
 import { OIDCProviderSettings } from '../components/OIDCProviderSettings';
 import { SecurityStatusCard } from '../components/SecurityStatusCard';
 import { APIBrowser } from '../components/APIBrowser';
@@ -7638,6 +7639,12 @@ export function SettingsPage() {
                   </div>
                 )}
 
+                {/* Two-factor authentication: admin removal for a locked-out user */}
+                <AdminDisableTwoFactor
+                  userId={editingUserId}
+                  username={usersData.find(u => u.id === editingUserId)?.username ?? userFormData.username}
+                />
+
                 {/* Groups Field */}
                 <div>
                   <label className="block text-sm font-medium text-white mb-2">{t('users.form.groups') || 'Groups'}</label>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AdditionalSection-D19VtSXK.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AmsUnitCard-DUVGmorU.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ArchivesPage-CrRBSiiu.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AreaChart-CNUYgfu2.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AssignToAmsModal-BSgNbELG.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/AuthContext-BQuxe7K9.js


+ 1 - 1
static/assets/Button-BOrMgOyo.js → static/assets/Button-BXOnmmh7.js

@@ -1 +1 @@
-import{C as e}from"./client-C5b1pVUf.js";var t=e();function n({variant:e=`primary`,size:n=`md`,className:r=``,children:i,...a}){return(0,t.jsx)(`button`,{className:`inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-bambu-dark disabled:opacity-50 disabled:cursor-not-allowed ${{primary:`bg-bambu-green hover:bg-bambu-green-light text-white focus:ring-bambu-green`,secondary:`bg-bambu-dark-tertiary hover:bg-bambu-gray-dark text-white focus:ring-bambu-gray`,danger:`bg-red-600 hover:bg-red-700 text-white focus:ring-red-500`,ghost:`bg-transparent hover:bg-bambu-dark-tertiary text-bambu-gray-light hover:text-white`}[e]} ${{sm:`px-3 py-1.5 text-sm gap-1.5 min-h-[44px] md:min-h-0`,md:`px-4 py-2 text-sm gap-2 min-h-[44px] md:min-h-0`,lg:`px-6 py-3 text-base gap-2 min-h-[48px] md:min-h-0`}[n]} ${r}`,...a,children:i})}export{n as t};
+import{C as e}from"./client-1U8CP3-A.js";var t=e();function n({variant:e=`primary`,size:n=`md`,className:r=``,children:i,...a}){return(0,t.jsx)(`button`,{className:`inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-bambu-dark disabled:opacity-50 disabled:cursor-not-allowed ${{primary:`bg-bambu-green hover:bg-bambu-green-light text-white focus:ring-bambu-green`,secondary:`bg-bambu-dark-tertiary hover:bg-bambu-gray-dark text-white focus:ring-bambu-gray`,danger:`bg-red-600 hover:bg-red-700 text-white focus:ring-red-500`,ghost:`bg-transparent hover:bg-bambu-dark-tertiary text-bambu-gray-light hover:text-white`}[e]} ${{sm:`px-3 py-1.5 text-sm gap-1.5 min-h-[44px] md:min-h-0`,md:`px-4 py-2 text-sm gap-2 min-h-[44px] md:min-h-0`,lg:`px-6 py-3 text-base gap-2 min-h-[48px] md:min-h-0`}[n]} ${r}`,...a,children:i})}export{n as t};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CamWallPage-Bsaem5Q5.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraDiagnoseModal-ORoWYJrl.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraPage-CUs9LkoT.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CameraWall-AZmzq7EY.js


+ 1 - 1
static/assets/Card-C6dM19X9.js → static/assets/Card-PxwWH-ub.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-C5b1pVUf.js";var r=e(n(),1),i=t(),a=(0,r.createContext)(`normal`);function o({density:e,children:t}){return(0,i.jsx)(a.Provider,{value:e,children:t})}function s({children:e,className:t=``,onClick:n,onContextMenu:r,...a}){return(0,i.jsx)(`div`,{className:`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${n?`cursor-pointer`:``} ${t}`,onClick:n,onContextMenu:r,...a,children:e})}function c({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`px-4 py-2.5`:`px-6 py-4`} border-b border-bambu-dark-tertiary ${t}`,children:e})}function l({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`p-4`:`p-6`} ${t}`,children:e})}export{c as i,l as n,o as r,s as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-1U8CP3-A.js";var r=e(n(),1),i=t(),a=(0,r.createContext)(`normal`);function o({density:e,children:t}){return(0,i.jsx)(a.Provider,{value:e,children:t})}function s({children:e,className:t=``,onClick:n,onContextMenu:r,...a}){return(0,i.jsx)(`div`,{className:`bg-bambu-dark-secondary rounded-xl border border-bambu-dark-tertiary card-shadow ${n?`cursor-pointer`:``} ${t}`,onClick:n,onContextMenu:r,...a,children:e})}function c({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`px-4 py-2.5`:`px-6 py-4`} border-b border-bambu-dark-tertiary ${t}`,children:e})}function l({children:e,className:t=``,dense:n}){let o=(0,r.useContext)(a)===`dense`;return(0,i.jsx)(`div`,{className:`${n??o?`p-4`:`p-6`} ${t}`,children:e})}export{c as i,l as n,o as r,s as t};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CartesianChart-DyXiX83C.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ColumnConfigModal-BWd2YJW7.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ConfigureAmsSlotModal-ByQX-C31.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ConnectAuthorizePage-Cqn2eQU3.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ContextMenu-CRsw4iUt.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/CopyButton-D37Ln8vd.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ErrorBarContext-BXvaZPsx.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ExternalLinkPage-DfmwdIWm.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
static/assets/FileManagerPage-B-PQF-jW.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/FileUploadModal-EXrQ0AVn.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/FinancePage-CyfUzRt9.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/GCodeViewerPage-B-n3uOHw.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/GcodeToolpathViewer-BvTY9h_R.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/GroupEditPage-IHdiAnyS.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ImagePreviewModal-DtQrNGik.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/InventoryPage-DLVRmNbY.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/LibraryTrashPage-BfmIe8XZ.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/LoginPage-BurzdSat.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/MaintenancePage-lKPEJczF.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ModelSourcesPage-8KjTi7f-.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/NotificationsPage-2xsgTLgp.js


+ 1 - 1
static/assets/OverlayFrame-BIdWPGNk.js → static/assets/OverlayFrame-C5o8QwmR.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r}from"./client-C5b1pVUf.js";var i=e(n(),1);function a(e,t=null,n=0){let[a,o]=(0,i.useState)(null);return(0,i.useEffect)(()=>{if(!e)return;let i=new AbortController,a;return r.getOverlayLogo(t,i.signal).then(e=>{i.signal.aborted||(e?(a=URL.createObjectURL(e),o({url:a,token:t,revision:n})):o(null))}).catch(()=>{i.signal.aborted||o(null)}),()=>{i.abort(),a&&URL.revokeObjectURL(a)}},[e,t,n]),e&&a?.token===t&&a.revision===n?a.url:null}var o={logo:!1,logoRevision:0,from:``,to:``};function s(e){return/^#[0-9a-f]{6}$/i.test(e)}function c(e,t){return e&&t&&s(e)&&s(t)?`linear-gradient(to right, ${e}, ${t})`:void 0}function l(e){if(e)return{backgroundImage:e,backgroundClip:`text`,WebkitBackgroundClip:`text`,color:`transparent`,WebkitTextFillColor:`transparent`}}var u={landscape:{width:1920,height:1080},portrait:{width:1080,height:1920}},d=t();function f({layout:e,preview:t=!1,children:n}){let r=(0,i.useRef)(null),[a,o]=(0,i.useState)(0),{width:s,height:c}=u[e];(0,i.useLayoutEffect)(()=>{let e=r.current;if(!e)return;let t=()=>o(Math.min(e.clientWidth/s,e.clientHeight/c));if(t(),typeof ResizeObserver>`u`)return window.addEventListener(`resize`,t),()=>window.removeEventListener(`resize`,t);let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[s,c]);let l={width:s,height:c,"--overlay-vmin":`10.8px`,"--overlay-height":`${c}px`,position:`absolute`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%) scale(${a})`,transformOrigin:`center`};return(0,d.jsx)(`div`,{ref:r,className:t?`relative w-full overflow-hidden`:`fixed inset-0 overflow-hidden`,style:t?{aspectRatio:`${s} / ${c}`}:void 0,children:(0,d.jsx)(`div`,{style:l,children:n})})}export{c as a,s as i,u as n,l as o,o as r,a as s,f as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r}from"./client-1U8CP3-A.js";var i=e(n(),1);function a(e,t=null,n=0){let[a,o]=(0,i.useState)(null);return(0,i.useEffect)(()=>{if(!e)return;let i=new AbortController,a;return r.getOverlayLogo(t,i.signal).then(e=>{i.signal.aborted||(e?(a=URL.createObjectURL(e),o({url:a,token:t,revision:n})):o(null))}).catch(()=>{i.signal.aborted||o(null)}),()=>{i.abort(),a&&URL.revokeObjectURL(a)}},[e,t,n]),e&&a?.token===t&&a.revision===n?a.url:null}var o={logo:!1,logoRevision:0,from:``,to:``};function s(e){return/^#[0-9a-f]{6}$/i.test(e)}function c(e,t){return e&&t&&s(e)&&s(t)?`linear-gradient(to right, ${e}, ${t})`:void 0}function l(e){if(e)return{backgroundImage:e,backgroundClip:`text`,WebkitBackgroundClip:`text`,color:`transparent`,WebkitTextFillColor:`transparent`}}var u={landscape:{width:1920,height:1080},portrait:{width:1080,height:1920}},d=t();function f({layout:e,preview:t=!1,children:n}){let r=(0,i.useRef)(null),[a,o]=(0,i.useState)(0),{width:s,height:c}=u[e];(0,i.useLayoutEffect)(()=>{let e=r.current;if(!e)return;let t=()=>o(Math.min(e.clientWidth/s,e.clientHeight/c));if(t(),typeof ResizeObserver>`u`)return window.addEventListener(`resize`,t),()=>window.removeEventListener(`resize`,t);let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[s,c]);let l={width:s,height:c,"--overlay-vmin":`10.8px`,"--overlay-height":`${c}px`,position:`absolute`,top:`50%`,left:`50%`,transform:`translate(-50%, -50%) scale(${a})`,transformOrigin:`center`};return(0,d.jsx)(`div`,{ref:r,className:t?`relative w-full overflow-hidden`:`fixed inset-0 overflow-hidden`,style:t?{aspectRatio:`${s} / ${c}`}:void 0,children:(0,d.jsx)(`div`,{style:l,children:n})})}export{c as a,s as i,u as n,l as o,o as r,a as s,f as t};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 1
static/assets/PdfPreviewModal-DMiJMUO5.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PhotoGalleryModal-pzgeQD_Y.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PrinterLocationsPage-BR0B7TF0.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/PrintersPage-CxQMHxc6.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProfilesPage-D1H_BJ94.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProjectDetailPage-Dm3fpK73.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ProjectsPage-BvIE1Umi.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/QueryClientProvider-DNxj-7q3.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/QueuePage-DcWQgG7d.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ReferenceLine-yMeXz9U9.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/RichTextEditor-D-_UM6sz.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SettingsPage-CymzMUkk.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SettingsPage-DAd2Rbkn.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SetupPage-Coue7Gnb.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SliceModal-CH7h2Sgw.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyAmsPage-BctrcVtm.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-u7BfdSCv.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyDashboard-D73GYLxE.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-D30YmeEZ.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 1
static/assets/SpoolBuddySettingsPage-CLU7n5Tj.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-CbXN_BXQ.js


+ 1 - 1
static/assets/SpoolIcon-BTzgxzF3.js → static/assets/SpoolIcon-Ckk9N7-6.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-C5b1pVUf.js";import{n as r}from"./filamentSwatchHelpers-D3oXQSGk.js";import{t as i}from"./spoolPaint-CrvpHqdD.js";var a=e(n(),1),o=t();function s({spool:e,inset:t,size:n,border:i}){let s=n>=80?`card`:`preview`,c=(0,a.useMemo)(()=>r({effectSize:s,rgba:e.rgba,extraColors:e.extra_colors,effectType:e.effect_type,subtype:e.subtype}),[s,e.rgba,e.extra_colors,e.effect_type,e.subtype]),l=`${t*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-paint`,className:`absolute rounded-full box-border`,style:{top:l,left:l,right:l,bottom:l,...c,backgroundPosition:`center`,border:i}})}function c({inset:e}){let t=`${e*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-shade`,className:`absolute rounded-full`,style:{top:t,left:t,right:t,bottom:t,backgroundColor:`rgba(0, 0, 0, 0.15)`}})}function l({color:e,isEmpty:t,size:n=32,spool:r}){return t?(0,o.jsx)(`div`,{className:`rounded-full border-2 border-dashed border-zinc-500 flex items-center justify-center`,style:{width:n,height:n},children:(0,o.jsx)(`div`,{className:`w-2 h-2 rounded-full bg-zinc-600`})}):i(r)?(0,o.jsxs)(`div`,{className:`relative shrink-0`,style:{width:n,height:n},children:[(0,o.jsx)(s,{spool:r,size:n,inset:2/32,border:`${1.5*n/32}px solid rgba(255, 255, 255, 0.7)`}),(0,o.jsx)(c,{inset:5/32})]}):(0,o.jsxs)(`svg`,{width:n,height:n,viewBox:`0 0 32 32`,children:[(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`14`,fill:e,stroke:`white`,strokeWidth:`1.5`,strokeOpacity:`0.7`}),(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`11`,fill:e,style:{filter:`brightness(0.85)`}})]})}export{s as n,c as r,l as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-1U8CP3-A.js";import{n as r}from"./filamentSwatchHelpers-D3oXQSGk.js";import{t as i}from"./spoolPaint-CrvpHqdD.js";var a=e(n(),1),o=t();function s({spool:e,inset:t,size:n,border:i}){let s=n>=80?`card`:`preview`,c=(0,a.useMemo)(()=>r({effectSize:s,rgba:e.rgba,extraColors:e.extra_colors,effectType:e.effect_type,subtype:e.subtype}),[s,e.rgba,e.extra_colors,e.effect_type,e.subtype]),l=`${t*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-paint`,className:`absolute rounded-full box-border`,style:{top:l,left:l,right:l,bottom:l,...c,backgroundPosition:`center`,border:i}})}function c({inset:e}){let t=`${e*100}%`;return(0,o.jsx)(`div`,{"data-testid":`spool-shade`,className:`absolute rounded-full`,style:{top:t,left:t,right:t,bottom:t,backgroundColor:`rgba(0, 0, 0, 0.15)`}})}function l({color:e,isEmpty:t,size:n=32,spool:r}){return t?(0,o.jsx)(`div`,{className:`rounded-full border-2 border-dashed border-zinc-500 flex items-center justify-center`,style:{width:n,height:n},children:(0,o.jsx)(`div`,{className:`w-2 h-2 rounded-full bg-zinc-600`})}):i(r)?(0,o.jsxs)(`div`,{className:`relative shrink-0`,style:{width:n,height:n},children:[(0,o.jsx)(s,{spool:r,size:n,inset:2/32,border:`${1.5*n/32}px solid rgba(255, 255, 255, 0.7)`}),(0,o.jsx)(c,{inset:5/32})]}):(0,o.jsxs)(`svg`,{width:n,height:n,viewBox:`0 0 32 32`,children:[(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`14`,fill:e,stroke:`white`,strokeWidth:`1.5`,strokeOpacity:`0.7`}),(0,o.jsx)(`circle`,{cx:`16`,cy:`16`,r:`11`,fill:e,style:{filter:`brightness(0.85)`}})]})}export{s as n,c as r,l as t};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SpreadsheetPreviewModal-DvAy1X-M.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/StatsPage-Qip2cyZj.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/StreamOverlayPage-CyHO7ILh.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/SystemInfoPage-CpJNOXPh.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/ToastContext-Bhj9DS2p.js


+ 1 - 1
static/assets/Toggle-B27GN7KJ.js → static/assets/Toggle-M-zinw5Z.js

@@ -1 +1 @@
-import{C as e}from"./client-C5b1pVUf.js";var t=e();function n({checked:e,onChange:n,disabled:r}){return(0,t.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:r,onClick:t=>{t.preventDefault(),t.stopPropagation(),r||n(!e)},className:`relative inline-flex w-11 h-7 md:w-9 md:h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${r?`bg-bambu-dark-tertiary/50 cursor-not-allowed opacity-50`:e?`bg-bambu-green cursor-pointer`:`bg-bambu-dark-tertiary cursor-pointer hover:bg-bambu-dark-tertiary/80`}`,children:(0,t.jsx)(`span`,{className:`pointer-events-none absolute top-[3px] md:top-[2px] left-[3px] md:left-[2px] w-5 h-5 md:w-4 md:h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${e?`translate-x-4`:`translate-x-0`}`})})}export{n as t};
+import{C as e}from"./client-1U8CP3-A.js";var t=e();function n({checked:e,onChange:n,disabled:r}){return(0,t.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e,disabled:r,onClick:t=>{t.preventDefault(),t.stopPropagation(),r||n(!e)},className:`relative inline-flex w-11 h-7 md:w-9 md:h-5 rounded-full transition-colors flex-shrink-0 focus:outline-none focus:ring-2 focus:ring-bambu-green focus:ring-offset-2 focus:ring-offset-bambu-dark ${r?`bg-bambu-dark-tertiary/50 cursor-not-allowed opacity-50`:e?`bg-bambu-green cursor-pointer`:`bg-bambu-dark-tertiary cursor-pointer hover:bg-bambu-dark-tertiary/80`}`,children:(0,t.jsx)(`span`,{className:`pointer-events-none absolute top-[3px] md:top-[2px] left-[3px] md:left-[2px] w-5 h-5 md:w-4 md:h-4 bg-white rounded-full shadow transition-transform duration-200 ease-in-out ${e?`translate-x-4`:`translate-x-0`}`})})}export{n as t};

+ 0 - 1
static/assets/arrow-down-Cxl6vzXB.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`arrow-down`,[[`path`,{d:`M12 5v14`,key:`s699le`}],[`path`,{d:`m19 12-7 7-7-7`,key:`1idqje`}]]);export{t};

+ 1 - 0
static/assets/arrow-down-VmQok7Hw.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-down`,[[`path`,{d:`M12 5v14`,key:`s699le`}],[`path`,{d:`m19 12-7 7-7-7`,key:`1idqje`}]]);export{t};

+ 1 - 0
static/assets/arrow-left-BcrTUh-S.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};

+ 0 - 1
static/assets/arrow-left-BdDYciL5.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`arrow-left`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]);export{t};

+ 0 - 1
static/assets/arrow-right-D8L4bue0.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]);export{t};

+ 1 - 0
static/assets/arrow-right-bfq0Qx0q.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]);export{t};

+ 0 - 1
static/assets/arrow-up-BA-zWepm.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`arrow-up`,[[`path`,{d:`m5 12 7-7 7 7`,key:`hav0vg`}],[`path`,{d:`M12 19V5`,key:`x0mq9r`}]]);export{t};

+ 1 - 0
static/assets/arrow-up-DXtx7YeZ.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-up`,[[`path`,{d:`m5 12 7-7 7 7`,key:`hav0vg`}],[`path`,{d:`M12 19V5`,key:`x0mq9r`}]]);export{t};

+ 1 - 1
static/assets/arrow-up-down-c4lzTQGp.js → static/assets/arrow-up-down-DUGhy30I.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`arrow-up-down`,[[`path`,{d:`m21 16-4 4-4-4`,key:`f6ql7i`}],[`path`,{d:`M17 20V4`,key:`1ejh1v`}],[`path`,{d:`m3 8 4-4 4 4`,key:`11wl7u`}],[`path`,{d:`M7 4v16`,key:`1glfcx`}]]);export{t};
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`arrow-up-down`,[[`path`,{d:`m21 16-4 4-4-4`,key:`f6ql7i`}],[`path`,{d:`M17 20V4`,key:`1ejh1v`}],[`path`,{d:`m3 8 4-4 4 4`,key:`11wl7u`}],[`path`,{d:`M7 4v16`,key:`1glfcx`}]]);export{t};

+ 1 - 1
static/assets/box-C-Edwu2W.js → static/assets/box-C9odJhAD.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`box`,[[`path`,{d:`M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z`,key:`hh9hay`}],[`path`,{d:`m3.3 7 8.7 5 8.7-5`,key:`g66t2b`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}]]);export{t};
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`box`,[[`path`,{d:`M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z`,key:`hh9hay`}],[`path`,{d:`m3.3 7 8.7 5 8.7-5`,key:`g66t2b`}],[`path`,{d:`M12 22V12`,key:`d0xqtd`}]]);export{t};

+ 1 - 1
static/assets/briefcase-aGpG9O_J.js → static/assets/briefcase--0-VcGda.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`briefcase`,[[`path`,{d:`M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16`,key:`jecpp`}],[`rect`,{width:`20`,height:`14`,x:`2`,y:`6`,rx:`2`,key:`i6l2r4`}]]);export{t};
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`briefcase`,[[`path`,{d:`M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16`,key:`jecpp`}],[`rect`,{width:`20`,height:`14`,x:`2`,y:`6`,rx:`2`,key:`i6l2r4`}]]);export{t};

+ 1 - 1
static/assets/calendar-evWoPgiR.js → static/assets/calendar-C_bF78FG.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`calendar`,[[`path`,{d:`M8 2v4`,key:`1cmpym`}],[`path`,{d:`M16 2v4`,key:`4m81vk`}],[`rect`,{width:`18`,height:`18`,x:`3`,y:`4`,rx:`2`,key:`1hopcy`}],[`path`,{d:`M3 10h18`,key:`8toen8`}]]);export{t};
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`calendar`,[[`path`,{d:`M8 2v4`,key:`1cmpym`}],[`path`,{d:`M16 2v4`,key:`4m81vk`}],[`rect`,{width:`18`,height:`18`,x:`3`,y:`4`,rx:`2`,key:`1hopcy`}],[`path`,{d:`M3 10h18`,key:`8toen8`}]]);export{t};

+ 1 - 0
static/assets/check-DLgNJCRi.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};

+ 0 - 1
static/assets/check-DeJLbG-z.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]);export{t};

+ 1 - 1
static/assets/chevrons-right-XdWmG843.js → static/assets/chevrons-right-CEkMMhQF.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-Br1j9igo.js";var t=e(`chevrons-left`,[[`path`,{d:`m11 17-5-5 5-5`,key:`13zhaf`}],[`path`,{d:`m18 17-5-5 5-5`,key:`h8a8et`}]]),n=e(`chevrons-right`,[[`path`,{d:`m6 17 5-5-5-5`,key:`xnjwq`}],[`path`,{d:`m13 17 5-5-5-5`,key:`17xmmf`}]]);export{t as n,n as t};
+import{t as e}from"./createLucideIcon-Cul4BqgW.js";var t=e(`chevrons-left`,[[`path`,{d:`m11 17-5-5 5-5`,key:`13zhaf`}],[`path`,{d:`m18 17-5-5 5-5`,key:`h8a8et`}]]),n=e(`chevrons-right`,[[`path`,{d:`m6 17 5-5-5-5`,key:`xnjwq`}],[`path`,{d:`m13 17 5-5-5-5`,key:`17xmmf`}]]);export{t as n,n as t};

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
static/assets/client-1U8CP3-A.js


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است