Explorar el Código

feat(notifications): Bark notification provider (#1495)

Bark is the open-source, account-free iOS push app (self-hostable
via bark-server). Configure with just the device key from the app;
the server URL defaults to the official api.day.app relay and
accepts a self-hosted instance. Optional settings: notification
Group, Sound, and iOS Interruption Level - Time Sensitive breaks
through scheduled summaries, Critical bypasses Silent mode and
Focus, Passive delivers silently.

bark-server can wrap failures in an HTTP 200 body ({"code": 400}),
so the sender checks the body code as well as the HTTP status.
Unknown interruption levels are dropped rather than forwarded.
maziggy hace 1 mes
padre
commit
62ba751278

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [1.2.6b1] - Unreleased
 
 ### Added
+- **Bark is now a notification provider (#1495)** — [Bark](https://github.com/Finb/Bark) is the open-source, account-free iOS push app (self-hostable via bark-server), popular especially with Chinese-speaking users. Configure it with just the device key from the app; the server URL defaults to the official `api.day.app` relay and accepts a self-hosted instance. Optional settings: notification **Group**, **Sound**, and iOS **Interruption Level** — Time Sensitive breaks through scheduled summaries, Critical bypasses Silent mode and Focus (useful for print-failure alerts), Passive delivers silently. Send failures wrapped in an HTTP 200 body by bark-server are detected and reported properly. Translated in all locales; wiki updated. Covered by backend and frontend tests.
 - **Home Assistant notifications can carry custom data fields (#1441)** — When a notification provider targets an HA notify service (e.g. `notify.mobile_app_myphone`), a new optional **Data (JSON)** field is forwarded as the service call's nested `data` object — the same place HA automations put mobile push options like `priority`, `ttl`, `channel`, and `group`. `ttl: 0` + `priority: high` make Android pushes arrive immediately instead of batched, and `channel` gives printer alerts their own notification channel/sound. The field is JSON (not key=value lines) so numbers stay numbers (`ttl: 0`) and nested options work. Validated on both ends: the UI rejects malformed JSON before saving, and the sender fails loudly with a clear message rather than posting a half-built payload. Only included when configured — the default persistent-notification path is unchanged, as its schema rejects unknown keys. Translated in all locales; wiki updated. Covered by backend and frontend tests.
 - **Energy usage now feeds the statistics that previously only knew about filament (#1432)** — Bambuddy has measured per-print energy via an attached smart plug for a while (the plug's lifetime counter is captured at print start and the delta stored with the print), but two stats surfaces ignored it. First, the **Most Expensive** record on the Statistics page ranked prints by filament cost alone, so a cheap-filament print with hours of heated-chamber time could never win; it now ranks by filament + measured energy cost (prints without a smart plug simply compete on filament cost, as before). Second, **Filament Trends** gained an **Energy Over Time** chart — per-day kWh (per-hour for short ranges, per-week for long ones), with the range's total kWh and energy cost in the header. The chart only appears when the selected range actually contains measured energy data, so setups without smart plugs see no change. The `/archives/slim` stats feed now carries each run's `energy_kwh`/`energy_cost`. Translated in all locales. Covered by backend and frontend tests.
 

+ 1 - 0
backend/app/schemas/notification.py

@@ -19,6 +19,7 @@ class ProviderType(StrEnum):
     DISCORD = "discord"
     WEBHOOK = "webhook"
     HOMEASSISTANT = "homeassistant"
+    BARK = "bark"
 
 
 class NotificationProviderBase(BaseModel):

+ 46 - 0
backend/app/services/notification_service.py

@@ -225,6 +225,8 @@ class NotificationService:
                 return await self._send_webhook(config, title, message)
             elif provider_type == "homeassistant":
                 return await self._send_homeassistant(config, title, message, db=db)
+            elif provider_type == "bark":
+                return await self._send_bark(config, title, message)
             else:
                 return False, f"Unknown provider type: {provider_type}"
         except Exception as e:
@@ -251,6 +253,48 @@ class NotificationService:
         else:
             return False, f"HTTP {response.status_code}: {response.text[:200]}"
 
+    async def _send_bark(self, config: dict, title: str, message: str) -> tuple[bool, str]:
+        """Send notification via Bark, the self-hostable iOS push service (#1495).
+
+        POSTs JSON to {server}/push. Defaults to the official api.day.app
+        relay; a self-hosted bark-server works by overriding the server URL.
+        """
+        server = (config.get("server") or "https://api.day.app").strip().rstrip("/")
+        device_key = (config.get("device_key") or "").strip()
+
+        if not device_key:
+            return False, "Device key is required"
+
+        payload: dict[str, Any] = {
+            "device_key": device_key,
+            "title": title,
+            "body": message,
+        }
+        group = (config.get("group") or "").strip()
+        if group:
+            payload["group"] = group
+        sound = (config.get("sound") or "").strip()
+        if sound:
+            payload["sound"] = sound
+        level = (config.get("level") or "").strip()
+        if level in ("active", "timeSensitive", "critical", "passive"):
+            payload["level"] = level
+
+        client = await self._get_client()
+        response = await client.post(f"{server}/push", json=payload)
+
+        if response.status_code == 200:
+            # bark-server can report failures inside an HTTP 200 body
+            # ({"code": 400, "message": ...}), so the status alone isn't proof.
+            try:
+                body = response.json()
+            except ValueError:
+                body = None
+            if isinstance(body, dict) and body.get("code") not in (200, None):
+                return False, f"Bark error {body.get('code')}: {str(body.get('message'))[:200]}"
+            return True, "Message sent successfully"
+        return False, f"HTTP {response.status_code}: {response.text[:200]}"
+
     async def _send_ntfy(
         self,
         config: dict,
@@ -812,6 +856,8 @@ class NotificationService:
                 )
             elif provider.provider_type == "homeassistant":
                 return await self._send_homeassistant(config, title, message, db=db)
+            elif provider.provider_type == "bark":
+                return await self._send_bark(config, title, message)
             else:
                 return False, f"Unknown provider type: {provider.provider_type}"
         except Exception as e:

+ 115 - 0
backend/tests/unit/services/test_notification_service.py

@@ -1090,6 +1090,121 @@ class TestHomeAssistantProvider:
         mock_send.assert_called_once()
 
 
+class TestBarkProvider:
+    """Bark (iOS push) provider (#1495)."""
+
+    @pytest.fixture
+    def service(self):
+        return NotificationService()
+
+    def _client_returning(self, status_code: int, json_body=None, text: str = ""):
+        mock_response = MagicMock()
+        mock_response.status_code = status_code
+        mock_response.text = text
+        if json_body is not None:
+            mock_response.json = MagicMock(return_value=json_body)
+        else:
+            mock_response.json = MagicMock(side_effect=ValueError("not json"))
+        mock_client = AsyncMock()
+        mock_client.post = AsyncMock(return_value=mock_response)
+        return mock_client
+
+    @pytest.mark.asyncio
+    async def test_send_bark_success_default_server(self, service):
+        """Minimal config posts to the official relay with device_key/title/body."""
+        mock_client = self._client_returning(200, {"code": 200, "message": "success"})
+
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            success, _ = await service._send_bark({"device_key": "abc123"}, "Title", "Body")
+
+        assert success is True
+        call_args = mock_client.post.call_args
+        assert call_args[0][0] == "https://api.day.app/push"
+        payload = call_args.kwargs.get("json")
+        assert payload == {"device_key": "abc123", "title": "Title", "body": "Body"}
+
+    @pytest.mark.asyncio
+    async def test_send_bark_options_and_custom_server(self, service):
+        """group/sound/level are forwarded; an unknown level is dropped rather
+        than sent; a self-hosted server URL (with trailing slash) is used."""
+        mock_client = self._client_returning(200, {"code": 200})
+
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            config = {
+                "device_key": "abc123",
+                "server": "https://bark.example.com/",
+                "group": "Bambuddy",
+                "sound": "minuet",
+                "level": "timeSensitive",
+            }
+            success, _ = await service._send_bark(config, "Title", "Body")
+
+        assert success is True
+        call_args = mock_client.post.call_args
+        assert call_args[0][0] == "https://bark.example.com/push"
+        payload = call_args.kwargs.get("json")
+        assert payload["group"] == "Bambuddy"
+        assert payload["sound"] == "minuet"
+        assert payload["level"] == "timeSensitive"
+
+        mock_client.post.reset_mock()
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            await service._send_bark({"device_key": "abc123", "level": "shouty"}, "Title", "Body")
+        assert "level" not in mock_client.post.call_args.kwargs.get("json")
+
+    @pytest.mark.asyncio
+    async def test_send_bark_missing_device_key(self, service):
+        mock_client = self._client_returning(200, {"code": 200})
+
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            success, message = await service._send_bark({}, "Title", "Body")
+
+        assert success is False
+        assert "Device key" in message
+        mock_client.post.assert_not_called()
+
+    @pytest.mark.asyncio
+    async def test_send_bark_error_in_200_body(self, service):
+        """bark-server can wrap a failure in HTTP 200; the body code must win."""
+        mock_client = self._client_returning(200, {"code": 400, "message": "device token invalid"})
+
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            success, message = await service._send_bark({"device_key": "bad"}, "Title", "Body")
+
+        assert success is False
+        assert "device token invalid" in message
+
+    @pytest.mark.asyncio
+    async def test_send_bark_http_error(self, service):
+        mock_client = self._client_returning(400, None, text="failed to get device token")
+
+        with patch.object(service, "_get_client", new_callable=AsyncMock) as mock_get_client:
+            mock_get_client.return_value = mock_client
+            success, message = await service._send_bark({"device_key": "bad"}, "Title", "Body")
+
+        assert success is False
+        assert "HTTP 400" in message
+
+    @pytest.mark.asyncio
+    async def test_send_to_provider_dispatches_bark(self, service):
+        provider = MagicMock()
+        provider.provider_type = "bark"
+        provider.config = json.dumps({"device_key": "abc123"})
+        provider.quiet_hours_enabled = False
+
+        with patch.object(service, "_send_bark", new_callable=AsyncMock) as mock_send:
+            mock_send.return_value = (True, "OK")
+            success, _ = await service._send_to_provider(provider, "Title", "Message", db=AsyncMock())
+
+        assert success is True
+        mock_send.assert_called_once()
+
+
 class TestNotificationVariableFallbacks:
     """Tests for notification variable fallback values."""
 

+ 1 - 1
frontend/scripts/check-i18n-parity.mjs

@@ -125,7 +125,7 @@ function isAlwaysAllowedIdentical(value) {
   if (/^https?:\/\//.test(value)) return true;          // URL
   if (/^ON,\s+true,\s+1$/.test(value)) return true;     // literal example "ON, true, 1"
   // Brand / technical names that ship verbatim everywhere.
-  if (/^(Bambuddy|BamBuddy|SpoolBuddy|Bambu Lab|Bambu Studio|Bambu Studio 2\.6\+|Bambu Studio sidecar URL|OrcaSlicer|OrcaSlicer sidecar URL|MakerWorld|Spoolman|\(Spoolman\)|Spoolman URL|Tailscale|GitHub|GitLab|Gitea|Forgejo|Discord|MQTT|FTP|HTTPS?|JSON|YAML|RTSP|TLS|SSL|CSRF|OIDC|SSO|SSO \/ OIDC|LDAP|TOTP|2FA|MFA|API|AMS|CRC|SHA256|SHA-256|kWh|MB|GB|KB|RGBA?|HSL|RGB|UTC|ISO|UI|HTTP|HTTP Method|H2D|H2D Pro|X1C|X1E|P1S|P1P|A1|A1 Mini|H2C|N3F|N3S|PETG|PLA|ABS|PA|TPU|PEI|PA-CF|PVA|HIPS|ASA|PC|PETG-HF|G\.code|G-code|gcode|cm³|°C|°F|GCODE|SOURCE|ntfy|Pushover|Telegram|Webhook|Webhook URL|Home Assistant|Home Assistant URL|CallMeBot\/WhatsApp|Bambuddy URL|Cool Plate|Cool Plate SuperTack|Engineering Plate|High Temp Plate|Smooth PEI Plate|Textured PEI Plate|Ext-L|Ext-R|ISO \(YYYY-MM-DD\))$/.test(value)) return true;
+  if (/^(Bambuddy|BamBuddy|SpoolBuddy|Bambu Lab|Bambu Studio|Bambu Studio 2\.6\+|Bambu Studio sidecar URL|OrcaSlicer|OrcaSlicer sidecar URL|MakerWorld|Spoolman|\(Spoolman\)|Spoolman URL|Tailscale|GitHub|GitLab|Gitea|Forgejo|Discord|MQTT|FTP|HTTPS?|JSON|YAML|RTSP|TLS|SSL|CSRF|OIDC|SSO|SSO \/ OIDC|LDAP|TOTP|2FA|MFA|API|AMS|CRC|SHA256|SHA-256|kWh|MB|GB|KB|RGBA?|HSL|RGB|UTC|ISO|UI|HTTP|HTTP Method|H2D|H2D Pro|X1C|X1E|P1S|P1P|A1|A1 Mini|H2C|N3F|N3S|PETG|PLA|ABS|PA|TPU|PEI|PA-CF|PVA|HIPS|ASA|PC|PETG-HF|G\.code|G-code|gcode|cm³|°C|°F|GCODE|SOURCE|ntfy|Pushover|Bark|Telegram|Webhook|Webhook URL|Home Assistant|Home Assistant URL|CallMeBot\/WhatsApp|Bambuddy URL|Cool Plate|Cool Plate SuperTack|Engineering Plate|High Temp Plate|Smooth PEI Plate|Textured PEI Plate|Ext-L|Ext-R|ISO \(YYYY-MM-DD\))$/.test(value)) return true;
   return false;
 }
 

+ 51 - 0
frontend/src/__tests__/components/AddNotificationModal.test.tsx

@@ -449,3 +449,54 @@ describe('AddNotificationModal — Home Assistant custom data (#1441)', () => {
     });
   });
 });
+
+describe('AddNotificationModal — Bark provider (#1495)', () => {
+  it('offers Bark in the provider select and renders its config fields', async () => {
+    render(
+      <AddNotificationModal
+        provider={buildProvider({ provider_type: 'bark', config: { device_key: 'abc123' } })}
+        onClose={() => undefined}
+      />,
+    );
+
+    await screen.findByDisplayValue('My ntfy');
+    expect(screen.getByRole('option', { name: 'Bark' })).toBeInTheDocument();
+    expect(screen.getByText(/device key/i)).toBeInTheDocument();
+    expect(screen.getByPlaceholderText('https://api.day.app')).toBeInTheDocument();
+    expect(screen.getByText(/interruption level/i)).toBeInTheDocument();
+  });
+
+  it('round-trips Bark options into config on save', async () => {
+    let captured: { config: Record<string, unknown> } | null = null;
+    server.use(
+      http.patch('*/api/v1/notifications/1', async ({ request }) => {
+        captured = (await request.json()) as { config: Record<string, unknown> };
+        return HttpResponse.json({ id: 1 });
+      }),
+    );
+
+    const onClose = vi.fn();
+    const user = userEvent.setup();
+    render(
+      <AddNotificationModal
+        provider={buildProvider({ provider_type: 'bark', config: { device_key: 'abc123' } })}
+        onClose={onClose}
+      />,
+    );
+
+    const groupInput = await screen.findByPlaceholderText('Bambuddy');
+    await user.type(groupInput, 'Printers');
+    const levelRow = screen.getByText(/interruption level/i).closest('div')!;
+    await user.selectOptions(within(levelRow).getByRole('combobox'), 'critical');
+
+    await user.click(screen.getByRole('button', { name: /^save$/i }));
+
+    await waitFor(() => expect(onClose).toHaveBeenCalled());
+    expect(captured).not.toBeNull();
+    expect(captured!.config).toMatchObject({
+      device_key: 'abc123',
+      group: 'Printers',
+      level: 'critical',
+    });
+  });
+});

+ 1 - 1
frontend/src/api/client.ts

@@ -2452,7 +2452,7 @@ export interface Filament {
 }
 
 // Notification Provider types
-export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook' | 'homeassistant';
+export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook' | 'homeassistant' | 'bark';
 
 export interface NotificationProvider {
   id: number;

+ 15 - 1
frontend/src/components/AddNotificationModal.tsx

@@ -12,7 +12,7 @@ interface AddNotificationModalProps {
   onClose: () => void;
 }
 
-const PROVIDER_VALUES: ProviderType[] = ['email', 'telegram', 'discord', 'ntfy', 'pushover', 'callmebot', 'webhook', 'homeassistant'];
+const PROVIDER_VALUES: ProviderType[] = ['email', 'telegram', 'discord', 'ntfy', 'pushover', 'bark', 'callmebot', 'webhook', 'homeassistant'];
 
 export function AddNotificationModal({ provider, onClose }: AddNotificationModalProps) {
   const { t } = useTranslation();
@@ -282,6 +282,20 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
           { key: 'service', label: 'Home Assistant Service', placeholder: 'notify.mobile_app_myphone', type: 'text', required: false },
           { key: 'data', label: 'Data (JSON, optional)', placeholder: '{"priority": "high", "ttl": 0, "channel": "3D Printing"}', type: 'textarea', required: false },
         ];
+      case 'bark':
+        return [
+          { key: 'device_key', label: 'Device Key', placeholder: 'Your Bark device key', type: 'text', required: true },
+          { key: 'server', label: 'Server URL', placeholder: 'https://api.day.app', type: 'text', required: false },
+          { key: 'group', label: 'Group', placeholder: 'Bambuddy', type: 'text', required: false },
+          { key: 'sound', label: 'Sound', placeholder: 'minuet', type: 'text', required: false },
+          { key: 'level', label: 'Interruption Level', type: 'select', required: false, options: [
+            { value: '', label: 'Default' },
+            { value: 'active', label: 'Active' },
+            { value: 'timeSensitive', label: 'Time Sensitive' },
+            { value: 'critical', label: 'Critical (bypasses Silent/Focus)' },
+            { value: 'passive', label: 'Passive (no sound)' },
+          ]},
+        ];
       default:
         return [];
     }

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

@@ -5421,6 +5421,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5432,6 +5433,7 @@ export default {
       callmebot: 'Kostenlose WhatsApp-Benachrichtigungen über CallMeBot',
       webhook: 'Generischer HTTP-POST an beliebige URL',
       homeassistant: 'Dauerhafte Benachrichtigungen im Home Assistant Dashboard',
+      bark: 'iOS-Push-Benachrichtigungen über Bark (selbst hostbar)',
     },
     // NotificationProviderCard
     lastSuccess: 'Zuletzt: {{date}}',

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

@@ -5465,6 +5465,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5476,6 +5477,7 @@ export default {
       callmebot: 'Free WhatsApp notifications via CallMeBot',
       webhook: 'Generic HTTP POST to any URL',
       homeassistant: 'Persistent notifications in Home Assistant dashboard',
+      bark: 'iOS push notifications via Bark (self-hostable)',
     },
     // NotificationProviderCard
     lastSuccess: 'Last: {{date}}',

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

@@ -5430,6 +5430,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5441,6 +5442,7 @@ export default {
       callmebot: 'Notificaciones de WhatsApp gratuitas mediante CallMeBot',
       webhook: 'POST HTTP genérico a cualquier URL',
       homeassistant: 'Notificaciones persistentes en el panel de Home Assistant',
+      bark: 'Notificaciones push de iOS mediante Bark (autoalojable)',
     },
     // NotificationProviderCard
     lastSuccess: 'Última: {{date}}',

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

@@ -5411,6 +5411,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5422,6 +5423,7 @@ export default {
       callmebot: 'Notifications WhatsApp gratuites via CallMeBot',
       webhook: 'POST HTTP générique vers n\'importe quelle URL',
       homeassistant: 'Notifications persistantes dans le tableau de bord Home Assistant',
+      bark: 'Notifications push iOS via Bark (auto-hébergeable)',
     },
     // NotificationProviderCard
     lastSuccess: 'Dernier : {{date}}',

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

@@ -5410,6 +5410,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5421,6 +5422,7 @@ export default {
       callmebot: 'Notifiche WhatsApp gratuite tramite CallMeBot',
       webhook: 'POST HTTP generico verso qualsiasi URL',
       homeassistant: 'Notifiche persistenti nella dashboard di Home Assistant',
+      bark: 'Notifiche push iOS tramite Bark (auto-ospitabile)',
     },
     // NotificationProviderCard
     lastSuccess: 'Ultimo: {{date}}',

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

@@ -5422,6 +5422,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5433,6 +5434,7 @@ export default {
       callmebot: 'CallMeBot経由の無料WhatsApp通知',
       webhook: '任意のURLへの汎用HTTP POST',
       homeassistant: 'Home Assistantダッシュボードの永続通知',
+      bark: 'Bark経由のiOSプッシュ通知(セルフホスト可能)',
     },
     // NotificationProviderCard
     lastSuccess: '最終: {{date}}',

+ 4 - 2
frontend/src/i18n/locales/ko.ts

@@ -5146,7 +5146,8 @@ export default {
       email: '이메일',
       discord: 'Discord',
       webhook: 'Webhook',
-      homeassistant: 'Home Assistant'
+      homeassistant: 'Home Assistant',
+      bark: 'Bark'
     },
     providerDescriptions: {
       email: 'SMTP 이메일 알림',
@@ -5156,7 +5157,8 @@ export default {
       pushover: '간단하고 신뢰할 수 있는 푸시 알림',
       callmebot: 'CallMeBot을 통한 무료 WhatsApp 알림',
       webhook: '모든 URL에 일반 HTTP POST',
-      homeassistant: 'Home Assistant 대시보드의 지속적인 알림'
+      homeassistant: 'Home Assistant 대시보드의 지속적인 알림',
+      bark: 'Bark를 통한 iOS 푸시 알림(셀프 호스팅 가능)'
     },
     lastSuccess: '마지막: {{date}}',
     error: '오류',

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

@@ -5410,6 +5410,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5421,6 +5422,7 @@ export default {
       callmebot: 'Notificações gratuitas via WhatsApp pelo CallMeBot',
       webhook: 'POST HTTP genérico para qualquer URL',
       homeassistant: 'Notificações persistentes no painel do Home Assistant',
+      bark: 'Notificações push do iOS via Bark (auto-hospedável)',
     },
     // NotificationProviderCard
     lastSuccess: 'Último: {{date}}',

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

@@ -5134,6 +5134,7 @@ export default {
       discord: "Discord",
       webhook: "Вебхук",
       homeassistant: "Home Assistant",
+      bark: "Bark",
     },
     providerDescriptions: {
       email: "Уведомления по электронной почте через SMTP",
@@ -5144,6 +5145,7 @@ export default {
       callmebot: "Бесплатные уведомления WhatsApp через CallMeBot",
       webhook: "Универсальный HTTP POST-запрос на любой URL",
       homeassistant: "Постоянные уведомления на панели Home Assistant",
+      bark: "iOS push-уведомления через Bark (можно разместить у себя)",
     },
     lastSuccess: "Последняя отправка: {{date}}",
     error: "Ошибка",

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

@@ -5383,6 +5383,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     providerDescriptions: {
       email: 'SMTP e-posta bildirimleri',
@@ -5393,6 +5394,7 @@ export default {
       callmebot: "CallMeBot üzerinden ücretsiz WhatsApp bildirimleri",
       webhook: 'Herhangi bir URL\'ye genel HTTP POST',
       homeassistant: 'Home Assistant gösterge panelinde kalıcı bildirimler',
+      bark: 'Bark ile iOS anlık bildirimleri (kendi sunucunuzda barındırılabilir)',
     },
     lastSuccess: 'Son: {{date}}',
     error: 'Hata',

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

@@ -5410,6 +5410,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5421,6 +5422,7 @@ export default {
       callmebot: '通过 CallMeBot 免费发送 WhatsApp 通知',
       webhook: '通用 HTTP POST 到任意 URL',
       homeassistant: 'Home Assistant 仪表板中的持久通知',
+      bark: '通过 Bark 推送 iOS 通知(可自建服务器)',
     },
     // NotificationProviderCard
     lastSuccess: '上次:{{date}}',

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

@@ -5410,6 +5410,7 @@ export default {
       discord: 'Discord',
       webhook: 'Webhook',
       homeassistant: 'Home Assistant',
+      bark: 'Bark',
     },
     // Provider descriptions
     providerDescriptions: {
@@ -5421,6 +5422,7 @@ export default {
       callmebot: '透過 CallMeBot 免費傳送 WhatsApp 通知',
       webhook: '通用 HTTP POST 到任意 URL',
       homeassistant: 'Home Assistant 儀表板中的持久通知',
+      bark: '透過 Bark 推送 iOS 通知(可自架伺服器)',
     },
     // NotificationProviderCard
     lastSuccess: '上次:{{date}}',

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-D6JvpN-_.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DW6dSr9L.js"></script>
+    <script type="module" crossorigin src="/assets/index-D6JvpN-_.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Di24iyOw.css">
   </head>
   <body>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio