Jelajahi Sumber

Security hardening (maziggy/bambuddy-security #17)

maziggy 1 Minggu lalu
induk
melakukan
929e89d6ce
100 mengubah file dengan 399 tambahan dan 45 penghapusan
  1. 22 6
      backend/app/api/routes/camera.py
  2. 20 6
      backend/app/core/logging_filters.py
  3. 33 1
      backend/app/schemas/printer.py
  4. 10 1
      backend/app/services/log_reader.py
  5. 124 0
      backend/tests/integration/test_printer_camera_url_credentials.py
  6. 50 0
      backend/tests/unit/services/test_log_reader_redacts_old_lines.py
  7. 11 0
      backend/tests/unit/test_log_credential_redaction.py
  8. 54 0
      backend/tests/unit/test_printer_camera_url_redaction.py
  9. 13 0
      backend/tests/unit/test_query_token_redaction.py
  10. 31 0
      frontend/src/__tests__/api/externalCameraTest.test.ts
  11. 4 2
      frontend/src/api/client.ts
  12. 0 0
      static/assets/AdditionalSection-DC0Av3w7.js
  13. 0 0
      static/assets/AmsUnitCard-B9_EY7BE.js
  14. 0 0
      static/assets/ArchivesPage-kqylcBaf.js
  15. 0 0
      static/assets/AreaChart-Bd3kKV8x.js
  16. 0 0
      static/assets/AssignToAmsModal-ubTeMl2l.js
  17. 1 1
      static/assets/Button-B2stjvVl.js
  18. 0 0
      static/assets/CamWallPage-CPsg6wOu.js
  19. 0 0
      static/assets/CameraDiagnoseModal-Dpzxpyi2.js
  20. 0 0
      static/assets/CameraPage-DbrwdFSg.js
  21. 0 0
      static/assets/CameraWall-Cg-tcG8a.js
  22. 1 1
      static/assets/Card-BvtE18YT.js
  23. 0 0
      static/assets/CartesianChart-DDT_IdbF.js
  24. 0 0
      static/assets/ColumnConfigModal-D9-mWmCK.js
  25. 0 0
      static/assets/ConfigureAmsSlotModal-_uJ4lnRw.js
  26. 0 0
      static/assets/ConnectAuthorizePage-Cbnmq9uU.js
  27. 0 0
      static/assets/ContextMenu-Txz1IAHf.js
  28. 0 0
      static/assets/CopyButton-CUjtC9jy.js
  29. 0 0
      static/assets/ErrorBarContext-DaqBSUpG.js
  30. 0 0
      static/assets/ExternalLinkPage-C9ayYR2c.js
  31. 0 1
      static/assets/FileManagerPage-CpyGd6KS.js
  32. 0 0
      static/assets/FileUploadModal-CQngu2lD.js
  33. 0 0
      static/assets/FinancePage-DU4PDuUl.js
  34. 0 0
      static/assets/GCodeViewerPage-DNB0zHyq.js
  35. 0 0
      static/assets/GcodeToolpathViewer-N4Eow1eU.js
  36. 0 0
      static/assets/GroupEditPage-36m4yQUS.js
  37. 0 0
      static/assets/ImagePreviewModal-C-YOYos3.js
  38. 0 0
      static/assets/InventoryPage-Cq5qEoac.js
  39. 0 0
      static/assets/LibraryTrashPage-BK-P5DpK.js
  40. 0 0
      static/assets/LoginPage-hIIM4ruZ.js
  41. 0 0
      static/assets/MaintenancePage-KE6t0cXU.js
  42. 0 0
      static/assets/ModelSourcesPage-foIS_xTF.js
  43. 0 0
      static/assets/NotificationsPage-z86UytXl.js
  44. 1 1
      static/assets/OverlayFrame-37LW4HkM.js
  45. 0 1
      static/assets/PdfPreviewModal-BN7qhxE9.js
  46. 0 0
      static/assets/PhotoGalleryModal-CCwSTH5i.js
  47. 0 0
      static/assets/PrinterLocationsPage-D4iDTG-5.js
  48. 0 0
      static/assets/PrintersPage-DzDBWo_r.js
  49. 0 0
      static/assets/ProfilesPage-CRnEdVJV.js
  50. 0 0
      static/assets/ProjectDetailPage-VUcaojMD.js
  51. 0 0
      static/assets/ProjectsPage-BVa1ZHtm.js
  52. 0 0
      static/assets/QueryClientProvider-DzWmuIjj.js
  53. 0 0
      static/assets/QueuePage-BogNVK4z.js
  54. 0 0
      static/assets/ReferenceLine-BHu5rFEs.js
  55. 0 0
      static/assets/RichTextEditor-BeG1yK0V.js
  56. 0 0
      static/assets/SettingsPage-CZLtafAF.js
  57. 0 0
      static/assets/SetupPage-CSofRX9z.js
  58. 0 0
      static/assets/SliceModal-BPmIA9M4.js
  59. 0 0
      static/assets/SpoolBuddyAmsPage-DXjp2X1h.js
  60. 0 0
      static/assets/SpoolBuddyCalibrationPage-Cl4jltKJ.js
  61. 0 0
      static/assets/SpoolBuddyDashboard-CTEPHd4-.js
  62. 0 0
      static/assets/SpoolBuddyInventoryPage-DZis57M1.js
  63. 1 1
      static/assets/SpoolBuddySettingsPage-Di2XQrih.js
  64. 0 0
      static/assets/SpoolBuddyWriteTagPage-wbJ19MS8.js
  65. 1 1
      static/assets/SpoolIcon-CilGQPyX.js
  66. 0 0
      static/assets/SpreadsheetPreviewModal-B25oY7Cq.js
  67. 0 0
      static/assets/StatsPage-vQ4Ptu8G.js
  68. 0 0
      static/assets/StreamOverlayPage-DjKdCokL.js
  69. 0 0
      static/assets/SystemInfoPage-Doq0F7Ax.js
  70. 0 0
      static/assets/ToastContext-BusjOw7N.js
  71. 1 1
      static/assets/Toggle-D6eRcIrQ.js
  72. 1 0
      static/assets/arrow-down-CrZSBK9b.js
  73. 0 1
      static/assets/arrow-down-DFHj0J4c.js
  74. 0 1
      static/assets/arrow-left-CJE7AWqE.js
  75. 1 0
      static/assets/arrow-left-CSvb6Y3A.js
  76. 0 1
      static/assets/arrow-right-BZsIRXWA.js
  77. 1 0
      static/assets/arrow-right-UsckSSBy.js
  78. 0 1
      static/assets/arrow-up-BvNwSX6Q.js
  79. 1 0
      static/assets/arrow-up-Rs6j04wk.js
  80. 1 1
      static/assets/arrow-up-down--x5UiiTo.js
  81. 1 1
      static/assets/box-FEAycrGc.js
  82. 1 1
      static/assets/briefcase-hCsuPkKt.js
  83. 1 1
      static/assets/calendar-DuDTxq8p.js
  84. 1 0
      static/assets/check-DMVASzXX.js
  85. 0 1
      static/assets/check-VKvTBKbR.js
  86. 1 1
      static/assets/chevrons-right-mXnDYbqq.js
  87. 0 0
      static/assets/client-C3LdD0iY.js
  88. 0 0
      static/assets/client-S4xOa3gM.js
  89. 1 1
      static/assets/cloud-off-9D5ISm0s.js
  90. 1 1
      static/assets/cog-D0lSWIp-.js
  91. 1 1
      static/assets/coins-CmRcTSq3.js
  92. 1 0
      static/assets/columns-2-BNCuaDKM.js
  93. 0 1
      static/assets/columns-2-DLXBhW8I.js
  94. 1 1
      static/assets/copy-BMNoF6UH.js
  95. 1 1
      static/assets/cpu-UQh6cW2O.js
  96. 1 1
      static/assets/createLucideIcon-DiLOIyaZ.js
  97. 1 1
      static/assets/dollar-sign-CnLUfjnL.js
  98. 1 1
      static/assets/droplet-D5UQKXET.js
  99. 1 1
      static/assets/ellipsis-vertical-DhwK3Pma.js
  100. 1 1
      static/assets/eye-off-BKHC5oyQ.js

+ 22 - 6
backend/app/api/routes/camera.py

@@ -12,8 +12,9 @@ import zlib
 from collections.abc import AsyncGenerator
 from typing import Annotated
 
-from fastapi import APIRouter, Depends, HTTPException, Query, Request
+from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
 from fastapi.responses import Response, StreamingResponse
+from pydantic import BaseModel
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -1649,30 +1650,45 @@ async def camera_status(
     }
 
 
+class ExternalCameraTestRequest(BaseModel):
+    url: str
+    camera_type: str
+
+
 @router.post("/{printer_id}/camera/external/test")
 async def test_external_camera(
     printer_id: int,
-    url: str,
-    camera_type: str,
+    body: ExternalCameraTestRequest | None = Body(default=None),
+    url: str | None = None,
+    camera_type: str | None = None,
     db: AsyncSession = Depends(get_db),
     _: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.CAMERA_VIEW),
 ):
     """Test external camera connection.
 
+    The URL comes in the body: it may carry the camera login, and a query
+    string ends up in the access log. The query form is still accepted from
+    an older page.
+
     Args:
         printer_id: Printer ID (for authorization)
-        url: Camera URL or USB device path to test
-        camera_type: Camera type ("mjpeg", "rtsp", "snapshot", "usb")
+        body / url: Camera URL or USB device path to test
+        body / camera_type: Camera type ("mjpeg", "rtsp", "snapshot", "usb")
 
     Returns:
         Dict with {success: bool, error?: str, resolution?: str}
     """
+    target_url = body.url if body else url
+    target_type = body.camera_type if body else camera_type
+    if not target_url or not target_type:
+        raise HTTPException(status_code=422, detail="url and camera_type are required")
+
     # Verify printer exists (for authorization)
     await get_printer_or_404(printer_id, db)
 
     from backend.app.services.external_camera import test_connection
 
-    return await test_connection(url, camera_type)
+    return await test_connection(target_url, target_type)
 
 
 @router.get("/{printer_id}/camera/check-plate")

+ 20 - 6
backend/app/core/logging_filters.py

@@ -40,13 +40,27 @@ import re
 # makes the work per offset constant. 63 is far above any real scheme (the
 # longest registered one is under 20 characters), and a longer pseudo-scheme
 # still gets its secret masked — the match simply starts from a later offset.
-URL_CREDENTIALS_PATTERN = re.compile(
-    r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.\-]{0,63}://)(?P<user>[^/:@\s]+):(?P<secret>[^/\s]+)@"
-)
+URL_CREDENTIALS_PATTERN = re.compile(r"(?P<scheme>[a-zA-Z][a-zA-Z0-9+.\-]{0,63}://)(?P<login>[^/\s]+)@")
+
+
+def _mask_login(match: re.Match[str]) -> str:
+    # The login runs to the last @ before the path. It may hold an unencoded ?
+    # or #, which cannot be told from a query: masked rather than risk it. Its
+    # user part (up to the first ':') survives; without a ':' it may be a
+    # token, so it all goes.
+    login = match.group("login")
+    shown = f"{login.split(':', 1)[0]}:[REDACTED]" if ":" in login else "[REDACTED]"
+    return f"{match.group('scheme')}{shown}@"
 
 
 def redact_url_credentials(text: str | None) -> str | None:
-    """Mask the password in every ``scheme://user:secret@host`` URL in *text*.
+    """Mask the login secret in every ``scheme://login@host`` URL in *text*.
+
+    The login runs to the last ``@`` before the path (``?`` and ``#``
+    included, when in doubt it is masked), so an empty user
+    (``:secret@``), an unencoded ``@`` in the user and a bare token
+    (``token@``, masked whole) are covered as well as ``user:secret@``.
+    Also used for camera URLs shown to those who may not edit the printer.
 
     Subprocesses echo their input URL back at us — ffmpeg prints the RTSP
     input in its ``Input #0`` line, so logging its stderr verbatim publishes
@@ -59,7 +73,7 @@ def redact_url_credentials(text: str | None) -> str | None:
     """
     if not text or "://" not in text or "@" not in text:
         return text
-    return URL_CREDENTIALS_PATTERN.sub(r"\g<scheme>\g<user>:[REDACTED]@", text)
+    return URL_CREDENTIALS_PATTERN.sub(_mask_login, text)
 
 
 # ``?token=<value>`` and its kin. The SPA passes its short-lived WebSocket and
@@ -70,7 +84,7 @@ def redact_url_credentials(text: str | None) -> str | None:
 # and from there anything that collects container logs -- the appliance's
 # support bundle among them. The value ends at the next ``&``, whitespace or
 # quote, which is where uvicorn's format closes the path.
-QUERY_TOKEN_PATTERN = re.compile(r"(?P<key>[?&](?:token|access_token|api_key)=)[^&\s\"']+")
+QUERY_TOKEN_PATTERN = re.compile(r"(?P<key>[?&](?:token|access_token|api_key|url)=)[^&\s\"']+")
 
 
 def redact_query_tokens(text: str | None) -> str | None:

+ 33 - 1
backend/app/schemas/printer.py

@@ -7,6 +7,26 @@ from backend.app.schemas.printer_location import normalize_location_name
 from backend.app.utils.printer_models import supports_nozzle_flow_type
 
 
+def _mask_url_login(url: str | None) -> str | None:
+    """A stored camera URL with its login secret masked.
+
+    One URL, not a log line: the login runs to the last ``@`` before the
+    first ``/`` after the scheme, whitespace, ``?`` and ``#`` included (when
+    in doubt it is masked). The user part up to the first ``:`` survives;
+    without a ``:`` the login may be a token and goes whole.
+    """
+    if not url or "://" not in url:
+        return url
+    start = url.index("://") + 3
+    slash = url.find("/", start)
+    end = len(url) if slash == -1 else slash
+    if "@" not in url[start:end]:
+        return url
+    login, host = url[start:end].rsplit("@", 1)
+    shown = f"{login.split(':', 1)[0]}:[REDACTED]" if ":" in login else "[REDACTED]"
+    return f"{url[:start]}{shown}@{host}{url[end:]}"
+
+
 class PrinterBase(BaseModel):
     name: str = Field(..., min_length=1, max_length=100)
     serial_number: str = Field(..., min_length=1, max_length=50)
@@ -159,9 +179,15 @@ class PrinterResponse(PrinterBase):
             )
         return cls(**data)
 
+    @field_validator("external_camera_url", "external_camera_snapshot_url", mode="after")
+    @classmethod
+    def _camera_credentials(cls, value: str | None) -> str | None:
+        """Readers see the camera URLs without the password in them."""
+        return _mask_url_login(value)
+
 
 class PrinterResponseWithSecret(PrinterResponse):
-    """PrinterResponse + access_code. Returned ONLY to callers with
+    """PrinterResponse + access_code and the full camera URLs. Returned ONLY to callers with
     PRINTERS_UPDATE (Admin / Operator JWTs, or single-trust auth-disabled mode).
 
     Viewers and API keys never receive this shape — they get the bare
@@ -171,6 +197,12 @@ class PrinterResponseWithSecret(PrinterResponse):
 
     access_code: str
 
+    @field_validator("external_camera_url", "external_camera_snapshot_url", mode="after")
+    @classmethod
+    def _camera_credentials(cls, value: str | None) -> str | None:
+        """Editors keep the full camera URLs, to edit them."""
+        return value
+
 
 class HMSErrorResponse(BaseModel):
     code: str

+ 10 - 1
backend/app/services/log_reader.py

@@ -14,7 +14,7 @@ from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from backend.app.core.config import settings
-from backend.app.core.logging_filters import URL_CREDENTIALS_PATTERN, redact_query_tokens
+from backend.app.core.logging_filters import URL_CREDENTIALS_PATTERN, redact_query_tokens, redact_url_credentials
 from backend.app.models.printer import Printer
 from backend.app.models.settings import Settings
 from backend.app.models.user import User
@@ -64,6 +64,13 @@ def parse_log_line(line: str) -> LogEntry | None:
     return None
 
 
+def _redacted(entry: LogEntry) -> LogEntry:
+    # Lines on disk may predate a redaction: masked as they are read, before
+    # any search, so a search cannot confirm a secret either.
+    entry.message = redact_url_credentials(redact_query_tokens(entry.message)) or ""
+    return entry
+
+
 def read_log_entries(
     limit: int = 200,
     level_filter: str | None = None,
@@ -95,6 +102,7 @@ def read_log_entries(
                 if parsed:
                     # Found a new log entry start
                     if current_entry:
+                        current_entry = _redacted(current_entry)
                         # Apply filters and add previous entry (without multi_line_buffer - it belongs to new entry)
                         should_include = True
 
@@ -130,6 +138,7 @@ def read_log_entries(
             # Don't forget the last (oldest) entry
             # Note: any remaining multi_line_buffer would be orphaned lines before the first entry
             if current_entry and len(entries) < limit:
+                current_entry = _redacted(current_entry)
                 should_include = True
                 if level_filter and current_entry.level.upper() != level_filter.upper():
                     should_include = False

+ 124 - 0
backend/tests/integration/test_printer_camera_url_credentials.py

@@ -0,0 +1,124 @@
+"""External camera credentials reach only those who may edit the printer.
+
+The camera login commonly sits in its URL (rtsp://user:password@host/...).
+Printer readers got both camera URLs as stored; the printer access code was
+already withheld from them. Readers now get the URLs with the password
+replaced; editors keep the full URLs they need to edit them.
+"""
+
+from __future__ import annotations
+
+import pytest
+from httpx import AsyncClient
+
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
+
+STREAM = "rtsp://cam:stream-pw@192.168.1.50:554/stream"
+SNAPSHOT = "http://cam:snap-pw@192.168.1.50/snapshot.jpg"
+
+
+async def _admin(client: AsyncClient) -> dict[str, str]:
+    await client.post(
+        "/api/v1/auth/setup",
+        json={"auth_enabled": True, "admin_username": "camadmin", "admin_password": "AdminPass1!"},
+    )
+    login = await client.post("/api/v1/auth/login", json={"username": "camadmin", "password": "AdminPass1!"})
+    return {"Authorization": f"Bearer {login.json()['access_token']}"}
+
+
+async def _viewer(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
+    group = await client.post(
+        "/api/v1/groups/", headers=admin, json={"name": "camview", "permissions": ["printers:read"]}
+    )
+    await client.post(
+        "/api/v1/users/",
+        headers=admin,
+        json={"username": "camviewer", "password": "UserPass1!", "group_ids": [group.json()["id"]]},
+    )
+    login = await client.post("/api/v1/auth/login", json={"username": "camviewer", "password": "UserPass1!"})
+    return {"Authorization": f"Bearer {login.json()['access_token']}"}
+
+
+async def _api_key(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
+    key = await client.post("/api/v1/api-keys/", headers=admin, json={"name": "reader", "can_read_status": True})
+    return {"X-API-Key": key.json()["key"]}
+
+
+@pytest.fixture
+async def camera_printer(printer_factory):
+    return await printer_factory(
+        name="Cam", external_camera_enabled=True, external_camera_url=STREAM, external_camera_snapshot_url=SNAPSHOT
+    )
+
+
+@pytest.mark.parametrize("who", ["viewer", "api_key"])
+async def test_a_reader_gets_the_camera_urls_without_their_passwords(async_client: AsyncClient, camera_printer, who):
+    admin = await _admin(async_client)
+    headers = await (_viewer if who == "viewer" else _api_key)(async_client, admin)
+
+    listed = await async_client.get("/api/v1/printers/", headers=headers)
+    single = await async_client.get(f"/api/v1/printers/{camera_printer.id}", headers=headers)
+
+    assert listed.status_code == 200 and single.status_code == 200, (listed.text, single.text)
+    for body in (listed.json()[0], single.json()):
+        assert "stream-pw" not in body["external_camera_url"]
+        assert "snap-pw" not in body["external_camera_snapshot_url"]
+        assert body["external_camera_url"].endswith("@192.168.1.50:554/stream")
+    assert "stream-pw" not in listed.text + single.text
+
+
+async def test_an_editor_gets_the_full_camera_urls(async_client: AsyncClient, camera_printer):
+    admin = await _admin(async_client)
+
+    single = (await async_client.get(f"/api/v1/printers/{camera_printer.id}", headers=admin)).json()
+    listed = (await async_client.get("/api/v1/printers/", headers=admin)).json()[0]
+
+    for body in (single, listed):
+        assert body["external_camera_url"] == STREAM
+        assert body["external_camera_snapshot_url"] == SNAPSHOT
+
+
+async def test_an_edit_leaves_the_stored_camera_url_alone(async_client: AsyncClient, camera_printer, db_session):
+    """The update's response is the reader shape; nothing it shows is written back."""
+    from backend.app.models.printer import Printer
+
+    printer_id = camera_printer.id
+    admin = await _admin(async_client)
+
+    response = await async_client.patch(f"/api/v1/printers/{printer_id}", headers=admin, json={"camera_rotation": 90})
+
+    assert response.status_code == 200, response.text
+    db_session.expire_all()
+    stored = await db_session.get(Printer, printer_id)
+    assert stored.external_camera_url == STREAM and stored.external_camera_snapshot_url == SNAPSHOT
+
+
+async def test_the_camera_test_takes_the_url_in_its_body_and_still_in_the_query(
+    async_client: AsyncClient, camera_printer
+):
+    """In the body it stays out of the access log; the query form is kept for a
+    cached older page."""
+    from unittest.mock import AsyncMock, patch
+
+    printer_id = camera_printer.id
+    admin = await _admin(async_client)
+
+    with patch(
+        "backend.app.services.external_camera.test_connection",
+        new_callable=AsyncMock,
+        return_value={"success": True},
+    ) as test:
+        body = await async_client.post(
+            f"/api/v1/printers/{printer_id}/camera/external/test",
+            headers=admin,
+            json={"url": STREAM, "camera_type": "rtsp"},
+        )
+        query = await async_client.post(
+            f"/api/v1/printers/{printer_id}/camera/external/test",
+            headers=admin,
+            params={"url": STREAM, "camera_type": "rtsp"},
+        )
+
+    assert body.status_code == 200, body.text
+    assert query.status_code == 200, query.text
+    assert [c.args for c in test.await_args_list] == [(STREAM, "rtsp"), (STREAM, "rtsp")]

+ 50 - 0
backend/tests/unit/services/test_log_reader_redacts_old_lines.py

@@ -0,0 +1,50 @@
+"""Log lines written before a redaction existed are masked when read.
+
+The filters only mask lines written from now on; ``bambuddy.log`` can still
+hold older ones with a camera login (an access line with the camera URL in
+its query, a URL in a shape an older pattern missed). Lines are masked as
+they are read, before the search filter, so a search cannot confirm a secret
+either.
+"""
+
+from backend.app.core.config import settings
+from backend.app.services.log_reader import read_log_entries
+
+ACCESS = (
+    "2026-10-01 10:00:00,000 INFO [uvicorn.access] [-] 127.0.0.1:1 - "
+    '"POST /api/v1/printers/1/camera/external/test'
+    '?url=rtsp%3A%2F%2Fadmin%3Aold-pw-1%40cam%2Fs&camera_type=rtsp HTTP/1.1" 200'
+)
+APP = (
+    "2026-10-01 10:00:01,000 INFO [backend.app.services.external_camera] [x] "
+    "Testing camera connection: type=rtsp, url=rtsp://:old-pw-2@10.0.0.5/s..."
+)
+TRACE = [
+    "2026-10-01 10:00:02,000 ERROR [backend.app.services.external_camera] [x] Stream failed",
+    "  Input #0, rtsp, from 'http://old-token-3@10.0.0.5/snap.jpg':",
+]
+
+
+def _write(tmp_path, monkeypatch):
+    (tmp_path / "bambuddy.log").write_text("\n".join([ACCESS, APP, *TRACE]) + "\n", encoding="utf-8")
+    monkeypatch.setattr(settings, "log_dir", tmp_path)
+
+
+def test_old_lines_come_back_masked(tmp_path, monkeypatch):
+    _write(tmp_path, monkeypatch)
+
+    entries, _total = read_log_entries(limit=50)
+
+    text = "\n".join(e.message for e in entries)
+    assert len(entries) == 3
+    for secret in ("old-pw-1", "old-pw-2", "old-token-3"):
+        assert secret not in text
+    assert "camera_type=rtsp" in text
+
+
+def test_a_search_cannot_confirm_a_secret(tmp_path, monkeypatch):
+    _write(tmp_path, monkeypatch)
+
+    for secret in ("old-pw-1", "old-pw-2", "old-token-3"):
+        entries, _total = read_log_entries(limit=50, search=secret)
+        assert entries == [], secret

+ 11 - 0
backend/tests/unit/test_log_credential_redaction.py

@@ -59,6 +59,17 @@ class TestRedactUrlCredentials:
         result = redact_url_credentials("rtsp://bblp:code@10.0.0.1/live/user@example")
         assert result == "rtsp://bblp:[REDACTED]@10.0.0.1/live/user@example"
 
+    def test_masks_every_login_shape_a_camera_accepts(self):
+        """An empty user, an unencoded @ in the user, a bare token: the login
+        is everything up to the last @ before the path."""
+        for url, expected in (
+            ("from 'rtsp://:pw123@10.0.0.5/s'", "from 'rtsp://:[REDACTED]@10.0.0.5/s'"),
+            ("rtsp://me@example.com:pw123@10.0.0.5/s", "rtsp://me@example.com:[REDACTED]@10.0.0.5/s"),
+            ("GET http://tok123@10.0.0.5/snap.jpg failed", "GET http://[REDACTED]@10.0.0.5/snap.jpg failed"),
+            ("rtsp://admin:pa:ss@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ):
+            assert redact_url_credentials(url) == expected
+
     def test_leaves_credential_free_text_alone(self):
         for untouched in (
             "Connection refused",

+ 54 - 0
backend/tests/unit/test_printer_camera_url_redaction.py

@@ -0,0 +1,54 @@
+"""Printer readers see the camera URLs without the login secret, whatever shape
+the login takes; editors see them as stored."""
+
+import pytest
+
+from backend.app.schemas.printer import PrinterResponse, PrinterResponseWithSecret
+
+BASE = {
+    "id": 1,
+    "name": "P",
+    "serial_number": "00M09A000000001",
+    "ip_address": "192.168.1.10",
+    "model": "X1C",
+    "is_active": True,
+    "created_at": "2026-10-13T00:00:00",
+    "updated_at": "2026-10-13T00:00:00",
+}
+
+
+@pytest.mark.parametrize(
+    ("stored", "shown"),
+    [
+        ("rtsp://admin:pw@10.0.0.5:554/s", "rtsp://admin:[REDACTED]@10.0.0.5:554/s"),
+        ("http://:pw@10.0.0.5/snap.jpg", "http://:[REDACTED]@10.0.0.5/snap.jpg"),
+        ("rtsp://me@example.com:pw@10.0.0.5/s", "rtsp://me@example.com:[REDACTED]@10.0.0.5/s"),
+        ("http://secrettoken@10.0.0.5/snap.jpg", "http://[REDACTED]@10.0.0.5/snap.jpg"),
+        ("rtsp://admin:p@ss@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ("rtsp://admin:pa:ss@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ("http://admin:pw@10.0.0.5", "http://admin:[REDACTED]@10.0.0.5"),
+        ("http://admin:pw@10.0.0.5/s?x=1#y", "http://admin:[REDACTED]@10.0.0.5/s?x=1#y"),
+        ("http://10.0.0.5/snap.jpg?a=b@c", "http://10.0.0.5/snap.jpg?a=b@c"),
+        ("http://10.0.0.5/path@thing", "http://10.0.0.5/path@thing"),
+        # Without a path, an @ in the query or fragment cannot be told from
+        # a password containing ? or #: masked rather than risk the password.
+        ("http://10.0.0.5?a=b@c", "http://[REDACTED]@c"),
+        ("http://10.0.0.5#a@b", "http://[REDACTED]@b"),
+        ("rtsp://admin:pa#ss@10.0.0.5:554/s", "rtsp://admin:[REDACTED]@10.0.0.5:554/s"),
+        ("rtsp://admin:pa?ss@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ("rtsp://admin:my pw@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ("rtsp://admin:my\tpw@10.0.0.5/s", "rtsp://admin:[REDACTED]@10.0.0.5/s"),
+        ("/dev/video0", "/dev/video0"),
+        ("", ""),
+        (None, None),
+    ],
+)
+def test_readers_never_see_the_login_secret(stored, shown):
+    reader = PrinterResponse(**BASE, external_camera_url=stored, external_camera_snapshot_url=stored)
+    editor = PrinterResponseWithSecret(
+        **BASE, access_code="12345678", external_camera_url=stored, external_camera_snapshot_url=stored
+    )
+
+    assert reader.external_camera_url == shown
+    assert reader.external_camera_snapshot_url == shown
+    assert editor.external_camera_url == stored

+ 13 - 0
backend/tests/unit/test_query_token_redaction.py

@@ -111,3 +111,16 @@ def test_support_bundle_sanitizer_masks_tokens():
     line = f'INFO: 192.168.255.4:0 - "WebSocket /api/v1/ws?token={TOKEN}" [accepted]'
     assert TOKEN not in sanitize_log_content(line)
     assert "token=[REDACTED]" in sanitize_log_content(line)
+
+
+def test_a_camera_url_in_the_query_is_masked():
+    """The camera test sent the camera URL, login included, in its query."""
+    line = (
+        '"POST /api/v1/printers/1/camera/external/test'
+        '?url=rtsp%3A%2F%2Fcam%3Apw123%40192.168.1.50%3A554%2Fs&camera_type=rtsp HTTP/1.1" 200'
+    )
+
+    result = redact_query_tokens(line)
+
+    assert "pw123" not in result
+    assert "camera_type=rtsp" in result

+ 31 - 0
frontend/src/__tests__/api/externalCameraTest.test.ts

@@ -0,0 +1,31 @@
+/**
+ * The camera test sends the camera URL, which may carry the camera login, in
+ * its body: a query string ends up in the server's access log.
+ */
+
+import { describe, it, expect, afterEach } from 'vitest';
+import { http, HttpResponse } from 'msw';
+import { server } from '../mocks/server';
+import { api } from '../../api/client';
+
+afterEach(() => server.resetHandlers());
+
+describe('testExternalCamera', () => {
+  it('sends the URL in the body, not the query', async () => {
+    let seenUrl = '';
+    let body: unknown = null;
+    server.use(
+      http.post('*/api/v1/printers/:id/camera/external/test', async ({ request }) => {
+        seenUrl = request.url;
+        body = await request.json();
+        return HttpResponse.json({ success: true });
+      }),
+    );
+
+    await api.testExternalCamera(3, 'rtsp://cam:pw123@10.0.0.5/s', 'rtsp');
+
+    expect(seenUrl).not.toContain('pw123');
+    expect(new URL(seenUrl).search).toBe('');
+    expect(body).toEqual({ url: 'rtsp://cam:pw123@10.0.0.5/s', camera_type: 'rtsp' });
+  });
+});

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

@@ -5126,9 +5126,11 @@ export const api = {
     // running (Obico polling, a snapshot) rather than a connection this test
     // opened — a single-reader camera is shared rather than opened twice, so
     // the result is real but says nothing about reaching the camera just now.
+    // In the body: the URL may carry the camera login, and a query string
+    // ends up in the server's access log.
     request<{ success: boolean; error?: string; resolution?: string; coalesced?: boolean }>(
-      `/printers/${printerId}/camera/external/test?url=${encodeURIComponent(url)}&camera_type=${encodeURIComponent(cameraType)}`,
-      { method: 'POST' }
+      `/printers/${printerId}/camera/external/test`,
+      { method: 'POST', body: JSON.stringify({ url, camera_type: cameraType }) }
     ),
 
   // Print Control

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/AdditionalSection-DC0Av3w7.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/AmsUnitCard-B9_EY7BE.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ArchivesPage-kqylcBaf.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/AreaChart-Bd3kKV8x.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/AssignToAmsModal-ubTeMl2l.js


+ 1 - 1
static/assets/Button-DSQo4CdN.js → static/assets/Button-B2stjvVl.js

@@ -1 +1 @@
-import{C as e}from"./client-S4xOa3gM.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-C3LdD0iY.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};

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CamWallPage-CPsg6wOu.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CameraDiagnoseModal-Dpzxpyi2.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CameraPage-DbrwdFSg.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CameraWall-Cg-tcG8a.js


+ 1 - 1
static/assets/Card-DzC7lYtY.js → static/assets/Card-BvtE18YT.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-S4xOa3gM.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-C3LdD0iY.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};

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CartesianChart-DDT_IdbF.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ColumnConfigModal-D9-mWmCK.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ConfigureAmsSlotModal-_uJ4lnRw.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ConnectAuthorizePage-Cbnmq9uU.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ContextMenu-Txz1IAHf.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/CopyButton-CUjtC9jy.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ErrorBarContext-DaqBSUpG.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ExternalLinkPage-C9ayYR2c.js


File diff ditekan karena terlalu besar
+ 0 - 1
static/assets/FileManagerPage-CpyGd6KS.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/FileUploadModal-CQngu2lD.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/FinancePage-DU4PDuUl.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/GCodeViewerPage-DNB0zHyq.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/GcodeToolpathViewer-N4Eow1eU.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/GroupEditPage-36m4yQUS.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ImagePreviewModal-C-YOYos3.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/InventoryPage-Cq5qEoac.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/LibraryTrashPage-BK-P5DpK.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/LoginPage-hIIM4ruZ.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/MaintenancePage-KE6t0cXU.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ModelSourcesPage-foIS_xTF.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/NotificationsPage-z86UytXl.js


+ 1 - 1
static/assets/OverlayFrame-DZh7brCf.js → static/assets/OverlayFrame-37LW4HkM.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n,a as r}from"./client-S4xOa3gM.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-C3LdD0iY.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};

File diff ditekan karena terlalu besar
+ 0 - 1
static/assets/PdfPreviewModal-BN7qhxE9.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/PhotoGalleryModal-CCwSTH5i.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/PrinterLocationsPage-D4iDTG-5.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/PrintersPage-DzDBWo_r.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ProfilesPage-CRnEdVJV.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ProjectDetailPage-VUcaojMD.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ProjectsPage-BVa1ZHtm.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/QueryClientProvider-DzWmuIjj.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/QueuePage-BogNVK4z.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ReferenceLine-BHu5rFEs.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/RichTextEditor-BeG1yK0V.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SettingsPage-CZLtafAF.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SetupPage-CSofRX9z.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SliceModal-BPmIA9M4.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DXjp2X1h.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpoolBuddyCalibrationPage-Cl4jltKJ.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpoolBuddyDashboard-CTEPHd4-.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-DZis57M1.js


File diff ditekan karena terlalu besar
+ 1 - 1
static/assets/SpoolBuddySettingsPage-Di2XQrih.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpoolBuddyWriteTagPage-wbJ19MS8.js


+ 1 - 1
static/assets/SpoolIcon-B6relYVL.js → static/assets/SpoolIcon-CilGQPyX.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{C as t,D as n}from"./client-S4xOa3gM.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-C3LdD0iY.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};

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SpreadsheetPreviewModal-B25oY7Cq.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/StatsPage-vQ4Ptu8G.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/StreamOverlayPage-DjKdCokL.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/SystemInfoPage-Doq0F7Ax.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/ToastContext-BusjOw7N.js


+ 1 - 1
static/assets/Toggle-Iw233J1p.js → static/assets/Toggle-D6eRcIrQ.js

@@ -1 +1 @@
-import{C as e}from"./client-S4xOa3gM.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-C3LdD0iY.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};

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

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

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

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

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

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

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

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-DiLOIyaZ.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-BZsIRXWA.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-UsckSSBy.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-DiLOIyaZ.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-BvNwSX6Q.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-Rs6j04wk.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-DiLOIyaZ.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-C4DI0IdD.js → static/assets/arrow-up-down--x5UiiTo.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-DiLOIyaZ.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-CWWI712L.js → static/assets/box-FEAycrGc.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-DiLOIyaZ.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-BUFvBlMG.js → static/assets/briefcase-hCsuPkKt.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-DiLOIyaZ.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-3OWHV_7b.js → static/assets/calendar-DuDTxq8p.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-DiLOIyaZ.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-DMVASzXX.js

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

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

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

+ 1 - 1
static/assets/chevrons-right-DJ3iYfD7.js → static/assets/chevrons-right-mXnDYbqq.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.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-DiLOIyaZ.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};

File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/client-C3LdD0iY.js


File diff ditekan karena terlalu besar
+ 0 - 0
static/assets/client-S4xOa3gM.js


+ 1 - 1
static/assets/cloud-off-D0oftpGg.js → static/assets/cloud-off-9D5ISm0s.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`cloud-off`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193`,key:`yfwify`}],[`path`,{d:`M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07`,key:`jlfiyv`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`cloud-off`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M5.782 5.782A7 7 0 0 0 9 19h8.5a4.5 4.5 0 0 0 1.307-.193`,key:`yfwify`}],[`path`,{d:`M21.532 16.5A4.5 4.5 0 0 0 17.5 10h-1.79A7.008 7.008 0 0 0 10 5.07`,key:`jlfiyv`}]]);export{t};

+ 1 - 1
static/assets/cog-CD-VDawn.js → static/assets/cog-D0lSWIp-.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`cog`,[[`path`,{d:`M11 10.27 7 3.34`,key:`16pf9h`}],[`path`,{d:`m11 13.73-4 6.93`,key:`794ttg`}],[`path`,{d:`M12 22v-2`,key:`1osdcq`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M14 12h8`,key:`4f43i9`}],[`path`,{d:`m17 20.66-1-1.73`,key:`eq3orb`}],[`path`,{d:`m17 3.34-1 1.73`,key:`2wel8s`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`m20.66 17-1.73-1`,key:`sg0v6f`}],[`path`,{d:`m20.66 7-1.73 1`,key:`1ow05n`}],[`path`,{d:`m3.34 17 1.73-1`,key:`nuk764`}],[`path`,{d:`m3.34 7 1.73 1`,key:`1ulond`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}],[`circle`,{cx:`12`,cy:`12`,r:`8`,key:`46899m`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`cog`,[[`path`,{d:`M11 10.27 7 3.34`,key:`16pf9h`}],[`path`,{d:`m11 13.73-4 6.93`,key:`794ttg`}],[`path`,{d:`M12 22v-2`,key:`1osdcq`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M14 12h8`,key:`4f43i9`}],[`path`,{d:`m17 20.66-1-1.73`,key:`eq3orb`}],[`path`,{d:`m17 3.34-1 1.73`,key:`2wel8s`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`m20.66 17-1.73-1`,key:`sg0v6f`}],[`path`,{d:`m20.66 7-1.73 1`,key:`1ow05n`}],[`path`,{d:`m3.34 17 1.73-1`,key:`nuk764`}],[`path`,{d:`m3.34 7 1.73 1`,key:`1ulond`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}],[`circle`,{cx:`12`,cy:`12`,r:`8`,key:`46899m`}]]);export{t};

+ 1 - 1
static/assets/coins-D9kL92mH.js → static/assets/coins-CmRcTSq3.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`coins`,[[`circle`,{cx:`8`,cy:`8`,r:`6`,key:`3yglwk`}],[`path`,{d:`M18.09 10.37A6 6 0 1 1 10.34 18`,key:`t5s6rm`}],[`path`,{d:`M7 6h1v4`,key:`1obek4`}],[`path`,{d:`m16.71 13.88.7.71-2.82 2.82`,key:`1rbuyh`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`coins`,[[`circle`,{cx:`8`,cy:`8`,r:`6`,key:`3yglwk`}],[`path`,{d:`M18.09 10.37A6 6 0 1 1 10.34 18`,key:`t5s6rm`}],[`path`,{d:`M7 6h1v4`,key:`1obek4`}],[`path`,{d:`m16.71 13.88.7.71-2.82 2.82`,key:`1rbuyh`}]]);export{t};

+ 1 - 0
static/assets/columns-2-BNCuaDKM.js

@@ -0,0 +1 @@
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`columns-2`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M12 3v18`,key:`108xh3`}]]);export{t};

+ 0 - 1
static/assets/columns-2-DLXBhW8I.js

@@ -1 +0,0 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`columns-2`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M12 3v18`,key:`108xh3`}]]);export{t};

+ 1 - 1
static/assets/copy-B-3YTgwv.js → static/assets/copy-BMNoF6UH.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]);export{t};

+ 1 - 1
static/assets/cpu-uturhn7b.js → static/assets/cpu-UQh6cW2O.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`cpu`,[[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M17 20v2`,key:`1rnc9c`}],[`path`,{d:`M17 2v2`,key:`11trls`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M2 17h2`,key:`7oei6x`}],[`path`,{d:`M2 7h2`,key:`asdhe0`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`M20 17h2`,key:`1fpfkl`}],[`path`,{d:`M20 7h2`,key:`1o8tra`}],[`path`,{d:`M7 20v2`,key:`4gnj0m`}],[`path`,{d:`M7 2v2`,key:`1i4yhu`}],[`rect`,{x:`4`,y:`4`,width:`16`,height:`16`,rx:`2`,key:`1vbyd7`}],[`rect`,{x:`8`,y:`8`,width:`8`,height:`8`,rx:`1`,key:`z9xiuo`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`cpu`,[[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M17 20v2`,key:`1rnc9c`}],[`path`,{d:`M17 2v2`,key:`11trls`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M2 17h2`,key:`7oei6x`}],[`path`,{d:`M2 7h2`,key:`asdhe0`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`M20 17h2`,key:`1fpfkl`}],[`path`,{d:`M20 7h2`,key:`1o8tra`}],[`path`,{d:`M7 20v2`,key:`4gnj0m`}],[`path`,{d:`M7 2v2`,key:`1i4yhu`}],[`rect`,{x:`4`,y:`4`,width:`16`,height:`16`,rx:`2`,key:`1vbyd7`}],[`rect`,{x:`8`,y:`8`,width:`8`,height:`8`,rx:`1`,key:`z9xiuo`}]]);export{t};

+ 1 - 1
static/assets/createLucideIcon-CozFoJIa.js → static/assets/createLucideIcon-DiLOIyaZ.js

@@ -1 +1 @@
-import{i as e}from"./chunk-aKtaBQYM.js";import{D as t}from"./client-S4xOa3gM.js";var n=e(t()),r=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),i=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),a=e=>{let t=i(e);return t.charAt(0).toUpperCase()+t.slice(1)},o=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),s=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0},c={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},l=(0,n.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:r=2,absoluteStrokeWidth:i,className:a=``,children:l,iconNode:u,...d},f)=>(0,n.createElement)(`svg`,{ref:f,...c,width:t,height:t,stroke:e,strokeWidth:i?Number(r)*24/Number(t):r,className:o(`lucide`,a),...!l&&!s(d)&&{"aria-hidden":`true`},...d},[...u.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])),u=(e,t)=>{let i=(0,n.forwardRef)(({className:i,...s},c)=>(0,n.createElement)(l,{ref:c,iconNode:t,className:o(`lucide-${r(a(e))}`,`lucide-${e}`,i),...s}));return i.displayName=a(e),i};export{u as t};
+import{i as e}from"./chunk-aKtaBQYM.js";import{D as t}from"./client-C3LdD0iY.js";var n=e(t()),r=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),i=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(e,t,n)=>n?n.toUpperCase():t.toLowerCase()),a=e=>{let t=i(e);return t.charAt(0).toUpperCase()+t.slice(1)},o=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),s=e=>{for(let t in e)if(t.startsWith(`aria-`)||t===`role`||t===`title`)return!0},c={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},l=(0,n.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:r=2,absoluteStrokeWidth:i,className:a=``,children:l,iconNode:u,...d},f)=>(0,n.createElement)(`svg`,{ref:f,...c,width:t,height:t,stroke:e,strokeWidth:i?Number(r)*24/Number(t):r,className:o(`lucide`,a),...!l&&!s(d)&&{"aria-hidden":`true`},...d},[...u.map(([e,t])=>(0,n.createElement)(e,t)),...Array.isArray(l)?l:[l]])),u=(e,t)=>{let i=(0,n.forwardRef)(({className:i,...s},c)=>(0,n.createElement)(l,{ref:c,iconNode:t,className:o(`lucide-${r(a(e))}`,`lucide-${e}`,i),...s}));return i.displayName=a(e),i};export{u as t};

+ 1 - 1
static/assets/dollar-sign-CUSJc1ZN.js → static/assets/dollar-sign-CnLUfjnL.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`dollar-sign`,[[`line`,{x1:`12`,x2:`12`,y1:`2`,y2:`22`,key:`7eqyqh`}],[`path`,{d:`M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6`,key:`1b0p4s`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`dollar-sign`,[[`line`,{x1:`12`,x2:`12`,y1:`2`,y2:`22`,key:`7eqyqh`}],[`path`,{d:`M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6`,key:`1b0p4s`}]]);export{t};

+ 1 - 1
static/assets/droplet-BZWV7pRJ.js → static/assets/droplet-D5UQKXET.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`droplet`,[[`path`,{d:`M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z`,key:`c7niix`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`droplet`,[[`path`,{d:`M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z`,key:`c7niix`}]]);export{t};

+ 1 - 1
static/assets/ellipsis-vertical-IoKm9sSo.js → static/assets/ellipsis-vertical-DhwK3Pma.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`ellipsis-vertical`,[[`circle`,{cx:`12`,cy:`12`,r:`1`,key:`41hilf`}],[`circle`,{cx:`12`,cy:`5`,r:`1`,key:`gxeob9`}],[`circle`,{cx:`12`,cy:`19`,r:`1`,key:`lyex9k`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`ellipsis-vertical`,[[`circle`,{cx:`12`,cy:`12`,r:`1`,key:`41hilf`}],[`circle`,{cx:`12`,cy:`5`,r:`1`,key:`gxeob9`}],[`circle`,{cx:`12`,cy:`19`,r:`1`,key:`lyex9k`}]]);export{t};

+ 1 - 1
static/assets/eye-off-BAETyyqx.js → static/assets/eye-off-BKHC5oyQ.js

@@ -1 +1 @@
-import{t as e}from"./createLucideIcon-CozFoJIa.js";var t=e(`eye-off`,[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]);export{t};
+import{t as e}from"./createLucideIcon-DiLOIyaZ.js";var t=e(`eye-off`,[[`path`,{d:`M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49`,key:`ct8e1f`}],[`path`,{d:`M14.084 14.158a3 3 0 0 1-4.242-4.242`,key:`151rxh`}],[`path`,{d:`M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143`,key:`13bj9a`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]);export{t};

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