From ecdf11f9e983404ac578ef59118dbc01d6e119f7 Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Sat, 4 Jul 2026 06:37:19 -0700 Subject: [PATCH 1/7] feat(users): add GET /v1/users/developer/webhook/{wtype}/health Developer webhook delivery health is recorded on every attempt (failure count, last success/failure timestamps, last status/error, auto-disable after sustained failures) but there was no way to read it back: the app side has get_app_webhook_health, the developer side had only writers, and the e2e test hand-reads the raw Redis hash. Add the getter plus a GET endpoint so a client can see a webhook's failure count and why it was auto-disabled. Reuses the existing Redis health hash; fail-open on error. --- backend/database/webhook_health.py | 17 +++ backend/routers/users.py | 38 +++++- backend/tests/unit/test_dev_webhook_health.py | 108 ++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 backend/tests/unit/test_dev_webhook_health.py diff --git a/backend/database/webhook_health.py b/backend/database/webhook_health.py index 4f19dae3905..b764ee40798 100644 --- a/backend/database/webhook_health.py +++ b/backend/database/webhook_health.py @@ -419,3 +419,20 @@ def record_dev_webhook_success(uid: str, wtype: object): r.expire(key, _HEALTH_TTL) except Exception as e: logger.warning(f'record_dev_webhook_success redis error uid={uid} type={wtype}: {e}') + + +def get_dev_webhook_health(uid: str, wtype) -> Optional[dict]: + """Health telemetry for a user's developer webhook of a given type, or None when nothing recorded. + + Fail-open: any Redis error returns None so a degraded Redis never breaks the read. The key and the + type stringify mirror record_dev_webhook_success/record_dev_webhook_failure so reads and writes align. + """ + try: + wtype_str = wtype.value if hasattr(wtype, 'value') else str(wtype) + key = f'dev_webhook_health:{uid}:{wtype_str}' + data = r.hgetall(key) + if not data: + return None + return {k.decode(): v.decode() for k, v in data.items()} + except Exception: + return None diff --git a/backend/routers/users.py b/backend/routers/users.py index 41006c9c643..373d020e8dd 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -26,7 +26,7 @@ from services.users.data_export import iter_user_data_export from services.users.account_deletion import background_wipe_user_data, start_account_deletion from database.app_review_config import should_hide_subscription_ui -from database.webhook_health import record_dev_webhook_success +from database.webhook_health import record_dev_webhook_success, get_dev_webhook_health from database.conversations import get_in_progress_conversation, get_conversation from database.redis_db import ( cache_user_geolocation, @@ -427,6 +427,42 @@ def enable_user_webhook_endpoint(wtype: WebhookType, uid: str = Depends(auth.get return {'status': 'ok'} +@router.get('/v1/users/developer/webhook/{wtype}/health', tags=['v1']) +def get_user_webhook_health_endpoint(wtype: WebhookType, uid: str = Depends(auth.get_current_user_uid)): + """Delivery health for a developer webhook: failure count, last success/failure timestamps + (unix seconds), last HTTP status, last error, and whether it was auto-disabled after sustained + failures. Fields are zeroed / false / null when nothing has been recorded yet.""" + + def _as_int(v): + try: + return int(v) + except (TypeError, ValueError): + return None + + health = get_dev_webhook_health(uid, wtype) + if not health: + return { + 'type': wtype.value, + 'has_data': False, + 'failure_count': 0, + 'last_success_at': None, + 'last_failure_at': None, + 'last_status': None, + 'last_error': None, + 'disabled': False, + } + return { + 'type': wtype.value, + 'has_data': True, + 'failure_count': _as_int(health.get('failure_count')) or 0, + 'last_success_at': _as_int(health.get('last_success_at')), + 'last_failure_at': _as_int(health.get('last_failure_at')), + 'last_status': _as_int(health.get('last_status')), + 'last_error': health.get('last_error') or None, + 'disabled': health.get('disabled') == '1', + } + + @router.get('/v1/users/developer/webhooks/status', tags=['v1'], response_model=UserWebhooksStatusResponse) def get_user_webhooks_status(uid: str = Depends(auth.get_current_user_uid)): # This only happens the first time because the user_webhook_status_db function will return None for existing users diff --git a/backend/tests/unit/test_dev_webhook_health.py b/backend/tests/unit/test_dev_webhook_health.py new file mode 100644 index 00000000000..b4489557714 --- /dev/null +++ b/backend/tests/unit/test_dev_webhook_health.py @@ -0,0 +1,108 @@ +"""Unit tests for GET /v1/users/developer/webhook/{wtype}/health. + +The db helper (get_dev_webhook_health) is verified directly against a patched Redis +proxy. The router endpoint's response mapping is verified in CI, where routers.users' +heavy STT imports resolve; locally those deps are absent, so the endpoint cases skip +while the db-helper cases still run. Uses the sanctioned seams (import + patch.object, +no sys.modules mutation). +""" + +import os + +os.environ.setdefault( + "ENCRYPTION_SECRET", + "omi_ZwB2ZNqB2HHpMK6wStk7sTpavJiPTFg7gXUHnc4tFABPU6pZ2c2DKgehtfgi4RZv", +) +os.environ.setdefault("OPENAI_API_KEY", "test-openai-key-not-real") + +from unittest.mock import patch + +import pytest + +import database.webhook_health as wh + +try: + from routers import users as users_router + + _USERS_IMPORTABLE = True +except Exception: # heavy STT deps unavailable locally; present in CI + users_router = None + _USERS_IMPORTABLE = False + + +class _Wtype: + """Stand-in for a WebhookType enum member (only .value is used).""" + + def __init__(self, value): + self.value = value + + +# --------------------------------------------------------------------------- +# db helper: get_dev_webhook_health +# --------------------------------------------------------------------------- +def test_db_health_none_when_no_data(): + with patch.object(wh, "r") as r: + r.hgetall.return_value = {} + assert wh.get_dev_webhook_health("u1", _Wtype("audio_bytes")) is None + + +def test_db_health_decodes_and_uses_value_key(): + with patch.object(wh, "r") as r: + r.hgetall.return_value = {b"failure_count": b"3", b"disabled": b"1", b"last_error": b"boom"} + out = wh.get_dev_webhook_health("u1", _Wtype("audio_bytes")) + assert out == {"failure_count": "3", "disabled": "1", "last_error": "boom"} + assert r.hgetall.call_args[0][0] == "dev_webhook_health:u1:audio_bytes" + + +def test_db_health_fail_open_on_redis_error(): + with patch.object(wh, "r") as r: + r.hgetall.side_effect = RuntimeError("redis down") + assert wh.get_dev_webhook_health("u1", _Wtype("audio_bytes")) is None + + +def test_db_health_stringifies_non_enum_type(): + with patch.object(wh, "r") as r: + r.hgetall.return_value = {b"failure_count": b"0"} + wh.get_dev_webhook_health("u1", "memory_created") + assert r.hgetall.call_args[0][0] == "dev_webhook_health:u1:memory_created" + + +# --------------------------------------------------------------------------- +# router endpoint mapping (runs in CI where routers.users imports) +# --------------------------------------------------------------------------- +@pytest.mark.skipif(not _USERS_IMPORTABLE, reason="routers.users heavy deps unavailable locally") +def test_endpoint_has_data_false_when_absent(): + with patch.object(users_router, "get_dev_webhook_health", return_value=None): + resp = users_router.get_user_webhook_health_endpoint(wtype=_Wtype("audio_bytes"), uid="u1") + assert resp == { + "type": "audio_bytes", + "has_data": False, + "failure_count": 0, + "last_success_at": None, + "last_failure_at": None, + "last_status": None, + "last_error": None, + "disabled": False, + } + + +@pytest.mark.skipif(not _USERS_IMPORTABLE, reason="routers.users heavy deps unavailable locally") +def test_endpoint_maps_recorded_fields(): + health = { + "failure_count": "4", + "last_success_at": "1700000000", + "last_failure_at": "", # reset -> None + "last_status": "500", + "last_error": "", # reset -> None + "disabled": "1", + } + with patch.object(users_router, "get_dev_webhook_health", return_value=health): + resp = users_router.get_user_webhook_health_endpoint(wtype=_Wtype("audio_bytes"), uid="u1") + assert resp["type"] == "audio_bytes" + assert resp["has_data"] is True + assert resp["failure_count"] == 4 + assert resp["last_success_at"] == 1700000000 + assert resp["last_failure_at"] is None + assert resp["last_status"] == 500 + assert resp["last_error"] is None + assert resp["disabled"] is True From 9e6373eab94afdf048b8bd480b3bc2fd93fc23bc Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Tue, 7 Jul 2026 06:02:06 -0700 Subject: [PATCH 2/7] chore: refresh app client contract for webhook health --- .../Sources/Generated/OmiApi.generated.swift | 21 ++- .../src/renderer/src/lib/omiApi.generated.ts | 40 ++++- docs/api-reference/app-client-openapi.json | 161 ++++++++++++++++++ .../lib/services/omi-api/omiApi.generated.ts | 40 ++++- web/app/src/lib/omiApi.generated.ts | 40 ++++- .../src/lib/omiApi.generated.ts | 40 ++++- 6 files changed, 337 insertions(+), 5 deletions(-) diff --git a/desktop/macos/Desktop/Sources/Generated/OmiApi.generated.swift b/desktop/macos/Desktop/Sources/Generated/OmiApi.generated.swift index fffb53b187e..a6ae4704374 100644 --- a/desktop/macos/Desktop/Sources/Generated/OmiApi.generated.swift +++ b/desktop/macos/Desktop/Sources/Generated/OmiApi.generated.swift @@ -6865,6 +6865,25 @@ public enum OmiAPI { return try JSONDecoder().decode(OmiAnyCodable.self, from: data) } + public static func getUserWebhookHealthEndpointV1UsersDeveloperWebhookWtypeHealthGet(client: OmiApiClient, wtype: String) async throws -> OmiAnyCodable { + let _path = "/v1/users/developer/webhook/\(wtype)/health" + guard var components = URLComponents(string: client.baseURL + _path) else { + throw OmiApiError.invalidURL + } + guard let url = components.url else { throw OmiApiError.invalidURL } + var req = URLRequest(url: url) + req.httpMethod = "GET" + if let token = client.token { + req.setValue("Bearer " + token, forHTTPHeaderField: "Authorization") + } + let (data, resp) = try await URLSession.shared.data(for: req) + guard let http = resp as? HTTPURLResponse else { throw OmiApiError.invalidURL } + guard (200..<300).contains(http.statusCode) else { + throw OmiApiError.httpError(status: http.statusCode, data: data) + } + return try JSONDecoder().decode(OmiAnyCodable.self, from: data) + } + public static func getUserWebhooksStatusV1UsersDeveloperWebhooksStatusGet(client: OmiApiClient) async throws -> OmiAnyCodable { let _path = "/v1/users/developer/webhooks/status" guard var components = URLComponents(string: client.baseURL + _path) else { @@ -8637,5 +8656,5 @@ public enum OmiAPI { return try JSONDecoder().decode(OmiAnyCodable.self, from: data) } - // Total: 343 Swift client methods generated. + // Total: 344 Swift client methods generated. } diff --git a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts index 1f6bc37664d..6ab7bff82ed 100644 --- a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts +++ b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts @@ -2837,6 +2837,17 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } +export interface UserWebhookHealthResponse { + disabled: boolean; + failure_count: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface UserWebhookUrlResponse { url?: string | null; } @@ -3314,6 +3325,7 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; + "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5978,6 +5990,17 @@ export interface OmiApiPaths { }; }; }; + "/v1/users/developer/webhook/{wtype}/health": { + get: { + operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; + responses: { + "200": UserWebhookHealthResponse; + "401": void; + "404": void; + "422": HTTPValidationError; + }; + }; + }; "/v1/users/developer/webhooks/status": { get: { operationId: "get_user_webhooks_status_v1_users_developer_webhooks_status_get"; @@ -11073,6 +11096,21 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { + const _base = init?.baseURL ?? ""; + const _path = `/v1/users/developer/webhook/${path.wtype}/health`; + const _search = ""; + const _res = await fetch(`${_base}${_path}${_search}`, { + method: "GET", + headers: { + ...(init?.token ? { Authorization: `Bearer ${init.token}` } : {}), + ...init?.headers, + }, + }); + if (!_res.ok) throw new OmiApiError(_res.status, _res); + return _res.status === 204 ? (undefined as any) : await _res.json(); +} + export async function get_user_webhooks_status_v1_users_developer_webhooks_status_get(init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhooks/status`; @@ -12413,4 +12451,4 @@ export async function get_speech_profile_v4_speech_profile_get(init?: OmiApiClie return _res.status === 204 ? (undefined as any) : await _res.json(); } -// Total: 343 client methods generated. +// Total: 344 client methods generated. diff --git a/docs/api-reference/app-client-openapi.json b/docs/api-reference/app-client-openapi.json index 105b238bc4c..081c350d95a 100644 --- a/docs/api-reference/app-client-openapi.json +++ b/docs/api-reference/app-client-openapi.json @@ -16725,6 +16725,78 @@ "title": "UserUsageResponse", "type": "object" }, + "UserWebhookHealthResponse": { + "properties": { + "disabled": { + "title": "Disabled", + "type": "boolean" + }, + "failure_count": { + "title": "Failure Count", + "type": "integer" + }, + "has_data": { + "title": "Has Data", + "type": "boolean" + }, + "last_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Error" + }, + "last_failure_at": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Failure At" + }, + "last_status": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Status" + }, + "last_success_at": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Success At" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type", + "has_data", + "failure_count", + "disabled" + ], + "title": "UserWebhookHealthResponse", + "type": "object" + }, "UserWebhookUrlResponse": { "properties": { "url": { @@ -39057,6 +39129,95 @@ ] } }, + "/v1/users/developer/webhook/{wtype}/health": { + "get": { + "description": "Delivery health for a developer webhook: failure count, last success/failure timestamps\n(unix seconds), last HTTP status, last error, and whether it was auto-disabled after sustained\nfailures. Fields are zeroed / false / null when nothing has been recorded yet.", + "operationId": "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get", + "parameters": [ + { + "in": "path", + "name": "wtype", + "required": true, + "schema": { + "$ref": "#/components/schemas/WebhookType" + } + }, + { + "in": "header", + "name": "authorization", + "required": false, + "schema": { + "title": "Authorization", + "type": "string" + } + }, + { + "in": "header", + "name": "X-App-Platform", + "required": false, + "schema": { + "title": "X-App-Platform", + "type": "string" + } + }, + { + "in": "header", + "name": "X-Device-Id-Hash", + "required": false, + "schema": { + "title": "X-Device-Id-Hash", + "type": "string" + } + }, + { + "in": "header", + "name": "X-App-Version", + "required": false, + "schema": { + "title": "X-App-Version", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserWebhookHealthResponse" + } + } + }, + "description": "Successful Response" + }, + "401": { + "$ref": "#/components/responses/Error401" + }, + "404": { + "$ref": "#/components/responses/Error404" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "firebaseBearer": [] + } + ], + "summary": "Get User Webhook Health Endpoint", + "tags": [ + "v1" + ] + } + }, "/v1/users/developer/webhooks/status": { "get": { "operationId": "get_user_webhooks_status_v1_users_developer_webhooks_status_get", diff --git a/web/admin/lib/services/omi-api/omiApi.generated.ts b/web/admin/lib/services/omi-api/omiApi.generated.ts index 1f6bc37664d..6ab7bff82ed 100644 --- a/web/admin/lib/services/omi-api/omiApi.generated.ts +++ b/web/admin/lib/services/omi-api/omiApi.generated.ts @@ -2837,6 +2837,17 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } +export interface UserWebhookHealthResponse { + disabled: boolean; + failure_count: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface UserWebhookUrlResponse { url?: string | null; } @@ -3314,6 +3325,7 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; + "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5978,6 +5990,17 @@ export interface OmiApiPaths { }; }; }; + "/v1/users/developer/webhook/{wtype}/health": { + get: { + operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; + responses: { + "200": UserWebhookHealthResponse; + "401": void; + "404": void; + "422": HTTPValidationError; + }; + }; + }; "/v1/users/developer/webhooks/status": { get: { operationId: "get_user_webhooks_status_v1_users_developer_webhooks_status_get"; @@ -11073,6 +11096,21 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { + const _base = init?.baseURL ?? ""; + const _path = `/v1/users/developer/webhook/${path.wtype}/health`; + const _search = ""; + const _res = await fetch(`${_base}${_path}${_search}`, { + method: "GET", + headers: { + ...(init?.token ? { Authorization: `Bearer ${init.token}` } : {}), + ...init?.headers, + }, + }); + if (!_res.ok) throw new OmiApiError(_res.status, _res); + return _res.status === 204 ? (undefined as any) : await _res.json(); +} + export async function get_user_webhooks_status_v1_users_developer_webhooks_status_get(init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhooks/status`; @@ -12413,4 +12451,4 @@ export async function get_speech_profile_v4_speech_profile_get(init?: OmiApiClie return _res.status === 204 ? (undefined as any) : await _res.json(); } -// Total: 343 client methods generated. +// Total: 344 client methods generated. diff --git a/web/app/src/lib/omiApi.generated.ts b/web/app/src/lib/omiApi.generated.ts index 1f6bc37664d..6ab7bff82ed 100644 --- a/web/app/src/lib/omiApi.generated.ts +++ b/web/app/src/lib/omiApi.generated.ts @@ -2837,6 +2837,17 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } +export interface UserWebhookHealthResponse { + disabled: boolean; + failure_count: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface UserWebhookUrlResponse { url?: string | null; } @@ -3314,6 +3325,7 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; + "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5978,6 +5990,17 @@ export interface OmiApiPaths { }; }; }; + "/v1/users/developer/webhook/{wtype}/health": { + get: { + operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; + responses: { + "200": UserWebhookHealthResponse; + "401": void; + "404": void; + "422": HTTPValidationError; + }; + }; + }; "/v1/users/developer/webhooks/status": { get: { operationId: "get_user_webhooks_status_v1_users_developer_webhooks_status_get"; @@ -11073,6 +11096,21 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { + const _base = init?.baseURL ?? ""; + const _path = `/v1/users/developer/webhook/${path.wtype}/health`; + const _search = ""; + const _res = await fetch(`${_base}${_path}${_search}`, { + method: "GET", + headers: { + ...(init?.token ? { Authorization: `Bearer ${init.token}` } : {}), + ...init?.headers, + }, + }); + if (!_res.ok) throw new OmiApiError(_res.status, _res); + return _res.status === 204 ? (undefined as any) : await _res.json(); +} + export async function get_user_webhooks_status_v1_users_developer_webhooks_status_get(init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhooks/status`; @@ -12413,4 +12451,4 @@ export async function get_speech_profile_v4_speech_profile_get(init?: OmiApiClie return _res.status === 204 ? (undefined as any) : await _res.json(); } -// Total: 343 client methods generated. +// Total: 344 client methods generated. diff --git a/web/personas-open-source/src/lib/omiApi.generated.ts b/web/personas-open-source/src/lib/omiApi.generated.ts index 1f6bc37664d..6ab7bff82ed 100644 --- a/web/personas-open-source/src/lib/omiApi.generated.ts +++ b/web/personas-open-source/src/lib/omiApi.generated.ts @@ -2837,6 +2837,17 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } +export interface UserWebhookHealthResponse { + disabled: boolean; + failure_count: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface UserWebhookUrlResponse { url?: string | null; } @@ -3314,6 +3325,7 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; + "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5978,6 +5990,17 @@ export interface OmiApiPaths { }; }; }; + "/v1/users/developer/webhook/{wtype}/health": { + get: { + operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; + responses: { + "200": UserWebhookHealthResponse; + "401": void; + "404": void; + "422": HTTPValidationError; + }; + }; + }; "/v1/users/developer/webhooks/status": { get: { operationId: "get_user_webhooks_status_v1_users_developer_webhooks_status_get"; @@ -11073,6 +11096,21 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { + const _base = init?.baseURL ?? ""; + const _path = `/v1/users/developer/webhook/${path.wtype}/health`; + const _search = ""; + const _res = await fetch(`${_base}${_path}${_search}`, { + method: "GET", + headers: { + ...(init?.token ? { Authorization: `Bearer ${init.token}` } : {}), + ...init?.headers, + }, + }); + if (!_res.ok) throw new OmiApiError(_res.status, _res); + return _res.status === 204 ? (undefined as any) : await _res.json(); +} + export async function get_user_webhooks_status_v1_users_developer_webhooks_status_get(init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhooks/status`; @@ -12413,4 +12451,4 @@ export async function get_speech_profile_v4_speech_profile_get(init?: OmiApiClie return _res.status === 204 ? (undefined as any) : await _res.json(); } -// Total: 343 client methods generated. +// Total: 344 client methods generated. From c004ee49137cee65e71885453e5be48497ccae09 Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Tue, 7 Jul 2026 06:09:32 -0700 Subject: [PATCH 3/7] chore: declare webhook health route policy --- backend/route_policy_manifest.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/route_policy_manifest.yaml b/backend/route_policy_manifest.yaml index b9d8b36d0a5..c44a6501482 100644 --- a/backend/route_policy_manifest.yaml +++ b/backend/route_policy_manifest.yaml @@ -72,3 +72,25 @@ routes: data_domain: user_profile deprecation: state: active + - route_type: http + method: GET + path: /v1/users/developer/webhook/{wtype}/health + policy: + review_status: reviewed + auth: + mechanisms: + - firebase_id_token + placement: dependency + scopes: [] + byok: not_applicable + rate_limit: + policy_name: none + key_subject: none + enforcement: none + placement: none + timeout_class: default_method + surface: first_party_app + visibility: first_party + data_domain: metrics + deprecation: + state: active From 17312d90db0b6680a67a75116b7b0f368f84dadf Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Tue, 7 Jul 2026 06:18:52 -0700 Subject: [PATCH 4/7] chore: add desktop changelog for webhook health client --- .../changelog/unreleased/20260707-dev-webhook-health-api.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 desktop/macos/changelog/unreleased/20260707-dev-webhook-health-api.json diff --git a/desktop/macos/changelog/unreleased/20260707-dev-webhook-health-api.json b/desktop/macos/changelog/unreleased/20260707-dev-webhook-health-api.json new file mode 100644 index 00000000000..95741153f10 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260707-dev-webhook-health-api.json @@ -0,0 +1,3 @@ +{ + "change": "Adds desktop API client support for developer webhook health." +} From ab4b7b0b6822049fd08c70857aca515b7ffa164f Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Fri, 10 Jul 2026 12:18:55 -0700 Subject: [PATCH 5/7] chore(openapi): regenerate app-client contract + client schemas for the dev webhook-health route Regenerates the app-client contract and TS client schemas so the Public Developer API contract check passes, with a desktop changelog fragment for the regenerated desktop client file. --- ...0260710-dev-webhook-health-api-client.json | 3 + .../src/renderer/src/lib/omiApi.generated.ts | 16 +--- docs/api-reference/app-client-openapi.json | 76 +------------------ .../lib/services/omi-api/omiApi.generated.ts | 16 +--- web/app/src/lib/omiApi.generated.ts | 16 +--- .../src/lib/omiApi.generated.ts | 16 +--- 6 files changed, 12 insertions(+), 131 deletions(-) create mode 100644 desktop/macos/changelog/unreleased/20260710-dev-webhook-health-api-client.json diff --git a/desktop/macos/changelog/unreleased/20260710-dev-webhook-health-api-client.json b/desktop/macos/changelog/unreleased/20260710-dev-webhook-health-api-client.json new file mode 100644 index 00000000000..431537ff151 --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260710-dev-webhook-health-api-client.json @@ -0,0 +1,3 @@ +{ + "change": "Add GET /v1/users/developer/webhook/{wtype}/health to the generated Omi API client." +} diff --git a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts index 6ab7bff82ed..aadce8aaaf1 100644 --- a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts +++ b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts @@ -2837,17 +2837,6 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } -export interface UserWebhookHealthResponse { - disabled: boolean; - failure_count: number; - has_data: boolean; - last_error?: string | null; - last_failure_at?: number | null; - last_status?: number | null; - last_success_at?: number | null; - type: string; -} - export interface UserWebhookUrlResponse { url?: string | null; } @@ -3325,7 +3314,6 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; - "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5994,7 +5982,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": UserWebhookHealthResponse; + "200": unknown; "401": void; "404": void; "422": HTTPValidationError; @@ -11096,7 +11084,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/docs/api-reference/app-client-openapi.json b/docs/api-reference/app-client-openapi.json index 081c350d95a..cc6190f2131 100644 --- a/docs/api-reference/app-client-openapi.json +++ b/docs/api-reference/app-client-openapi.json @@ -16725,78 +16725,6 @@ "title": "UserUsageResponse", "type": "object" }, - "UserWebhookHealthResponse": { - "properties": { - "disabled": { - "title": "Disabled", - "type": "boolean" - }, - "failure_count": { - "title": "Failure Count", - "type": "integer" - }, - "has_data": { - "title": "Has Data", - "type": "boolean" - }, - "last_error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Last Error" - }, - "last_failure_at": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Last Failure At" - }, - "last_status": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Last Status" - }, - "last_success_at": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Last Success At" - }, - "type": { - "title": "Type", - "type": "string" - } - }, - "required": [ - "type", - "has_data", - "failure_count", - "disabled" - ], - "title": "UserWebhookHealthResponse", - "type": "object" - }, "UserWebhookUrlResponse": { "properties": { "url": { @@ -39183,9 +39111,7 @@ "200": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/UserWebhookHealthResponse" - } + "schema": {} } }, "description": "Successful Response" diff --git a/web/admin/lib/services/omi-api/omiApi.generated.ts b/web/admin/lib/services/omi-api/omiApi.generated.ts index 6ab7bff82ed..aadce8aaaf1 100644 --- a/web/admin/lib/services/omi-api/omiApi.generated.ts +++ b/web/admin/lib/services/omi-api/omiApi.generated.ts @@ -2837,17 +2837,6 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } -export interface UserWebhookHealthResponse { - disabled: boolean; - failure_count: number; - has_data: boolean; - last_error?: string | null; - last_failure_at?: number | null; - last_status?: number | null; - last_success_at?: number | null; - type: string; -} - export interface UserWebhookUrlResponse { url?: string | null; } @@ -3325,7 +3314,6 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; - "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5994,7 +5982,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": UserWebhookHealthResponse; + "200": unknown; "401": void; "404": void; "422": HTTPValidationError; @@ -11096,7 +11084,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/web/app/src/lib/omiApi.generated.ts b/web/app/src/lib/omiApi.generated.ts index 6ab7bff82ed..aadce8aaaf1 100644 --- a/web/app/src/lib/omiApi.generated.ts +++ b/web/app/src/lib/omiApi.generated.ts @@ -2837,17 +2837,6 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } -export interface UserWebhookHealthResponse { - disabled: boolean; - failure_count: number; - has_data: boolean; - last_error?: string | null; - last_failure_at?: number | null; - last_status?: number | null; - last_success_at?: number | null; - type: string; -} - export interface UserWebhookUrlResponse { url?: string | null; } @@ -3325,7 +3314,6 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; - "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5994,7 +5982,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": UserWebhookHealthResponse; + "200": unknown; "401": void; "404": void; "422": HTTPValidationError; @@ -11096,7 +11084,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/web/personas-open-source/src/lib/omiApi.generated.ts b/web/personas-open-source/src/lib/omiApi.generated.ts index 6ab7bff82ed..aadce8aaaf1 100644 --- a/web/personas-open-source/src/lib/omiApi.generated.ts +++ b/web/personas-open-source/src/lib/omiApi.generated.ts @@ -2837,17 +2837,6 @@ export interface UserUsageResponse { yearly?: UsageStats | null; } -export interface UserWebhookHealthResponse { - disabled: boolean; - failure_count: number; - has_data: boolean; - last_error?: string | null; - last_failure_at?: number | null; - last_status?: number | null; - last_success_at?: number | null; - type: string; -} - export interface UserWebhookUrlResponse { url?: string | null; } @@ -3325,7 +3314,6 @@ export interface OmiApiSchemas { "UserStatusResponse": UserStatusResponse; "UserSubscriptionResponse": UserSubscriptionResponse; "UserUsageResponse": UserUsageResponse; - "UserWebhookHealthResponse": UserWebhookHealthResponse; "UserWebhookUrlResponse": UserWebhookUrlResponse; "UserWebhooksStatusResponse": UserWebhooksStatusResponse; "ValidationError": ValidationError; @@ -5994,7 +5982,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": UserWebhookHealthResponse; + "200": unknown; "401": void; "404": void; "422": HTTPValidationError; @@ -11096,7 +11084,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; From 505683250cf55907bc299f44549d46a135080b60 Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Fri, 10 Jul 2026 20:56:46 -0700 Subject: [PATCH 6/7] fix(users): model dev webhook health response + regen contract incl Dart --- backend/routers/users.py | 13 +++- .../src/renderer/src/lib/omiApi.generated.ts | 16 +++- docs/api-reference/app-client-openapi.json | 76 ++++++++++++++++++- .../lib/services/omi-api/omiApi.generated.ts | 16 +++- web/app/src/lib/omiApi.generated.ts | 16 +++- .../src/lib/omiApi.generated.ts | 16 +++- 6 files changed, 143 insertions(+), 10 deletions(-) diff --git a/backend/routers/users.py b/backend/routers/users.py index 373d020e8dd..83d8df51250 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -427,7 +427,18 @@ def enable_user_webhook_endpoint(wtype: WebhookType, uid: str = Depends(auth.get return {'status': 'ok'} -@router.get('/v1/users/developer/webhook/{wtype}/health', tags=['v1']) +class DevWebhookHealthResponse(BaseModel): + type: str + has_data: bool + failure_count: int = 0 + last_success_at: Optional[int] = None + last_failure_at: Optional[int] = None + last_status: Optional[int] = None + last_error: Optional[str] = None + disabled: bool = False + + +@router.get('/v1/users/developer/webhook/{wtype}/health', tags=['v1'], response_model=DevWebhookHealthResponse) def get_user_webhook_health_endpoint(wtype: WebhookType, uid: str = Depends(auth.get_current_user_uid)): """Delivery health for a developer webhook: failure count, last success/failure timestamps (unix seconds), last HTTP status, last error, and whether it was auto-disabled after sustained diff --git a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts index c4e0c9f6490..1c3ca8e36d1 100644 --- a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts +++ b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts @@ -1371,6 +1371,17 @@ export interface DevApiKeyCreated { scopes?: Array | null; } +export interface DevWebhookHealthResponse { + disabled?: boolean; + failure_count?: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface DeveloperActionItem { completed: boolean; completed_at?: string | null; @@ -3840,6 +3851,7 @@ export interface OmiApiSchemas { "DevApiKey": DevApiKey; "DevApiKeyCreate": DevApiKeyCreate; "DevApiKeyCreated": DevApiKeyCreated; + "DevWebhookHealthResponse": DevWebhookHealthResponse; "DeveloperActionItem": DeveloperActionItem; "DeveloperConversation": DeveloperConversation; "DeveloperConversationActionItem": DeveloperConversationActionItem; @@ -7028,7 +7040,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": unknown; + "200": DevWebhookHealthResponse; "401": void; "404": void; "422": HTTPValidationError; @@ -13487,7 +13499,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/docs/api-reference/app-client-openapi.json b/docs/api-reference/app-client-openapi.json index 0202c502fb4..a086276e7a8 100644 --- a/docs/api-reference/app-client-openapi.json +++ b/docs/api-reference/app-client-openapi.json @@ -8691,6 +8691,78 @@ "title": "DevApiKeyCreated", "type": "object" }, + "DevWebhookHealthResponse": { + "properties": { + "disabled": { + "default": false, + "title": "Disabled", + "type": "boolean" + }, + "failure_count": { + "default": 0, + "title": "Failure Count", + "type": "integer" + }, + "has_data": { + "title": "Has Data", + "type": "boolean" + }, + "last_error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Last Error" + }, + "last_failure_at": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Failure At" + }, + "last_status": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Status" + }, + "last_success_at": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Last Success At" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "type", + "has_data" + ], + "title": "DevWebhookHealthResponse", + "type": "object" + }, "DeveloperActionItem": { "properties": { "completed": { @@ -46551,7 +46623,9 @@ "200": { "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/DevWebhookHealthResponse" + } } }, "description": "Successful Response" diff --git a/web/admin/lib/services/omi-api/omiApi.generated.ts b/web/admin/lib/services/omi-api/omiApi.generated.ts index c4e0c9f6490..1c3ca8e36d1 100644 --- a/web/admin/lib/services/omi-api/omiApi.generated.ts +++ b/web/admin/lib/services/omi-api/omiApi.generated.ts @@ -1371,6 +1371,17 @@ export interface DevApiKeyCreated { scopes?: Array | null; } +export interface DevWebhookHealthResponse { + disabled?: boolean; + failure_count?: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface DeveloperActionItem { completed: boolean; completed_at?: string | null; @@ -3840,6 +3851,7 @@ export interface OmiApiSchemas { "DevApiKey": DevApiKey; "DevApiKeyCreate": DevApiKeyCreate; "DevApiKeyCreated": DevApiKeyCreated; + "DevWebhookHealthResponse": DevWebhookHealthResponse; "DeveloperActionItem": DeveloperActionItem; "DeveloperConversation": DeveloperConversation; "DeveloperConversationActionItem": DeveloperConversationActionItem; @@ -7028,7 +7040,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": unknown; + "200": DevWebhookHealthResponse; "401": void; "404": void; "422": HTTPValidationError; @@ -13487,7 +13499,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/web/app/src/lib/omiApi.generated.ts b/web/app/src/lib/omiApi.generated.ts index c4e0c9f6490..1c3ca8e36d1 100644 --- a/web/app/src/lib/omiApi.generated.ts +++ b/web/app/src/lib/omiApi.generated.ts @@ -1371,6 +1371,17 @@ export interface DevApiKeyCreated { scopes?: Array | null; } +export interface DevWebhookHealthResponse { + disabled?: boolean; + failure_count?: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface DeveloperActionItem { completed: boolean; completed_at?: string | null; @@ -3840,6 +3851,7 @@ export interface OmiApiSchemas { "DevApiKey": DevApiKey; "DevApiKeyCreate": DevApiKeyCreate; "DevApiKeyCreated": DevApiKeyCreated; + "DevWebhookHealthResponse": DevWebhookHealthResponse; "DeveloperActionItem": DeveloperActionItem; "DeveloperConversation": DeveloperConversation; "DeveloperConversationActionItem": DeveloperConversationActionItem; @@ -7028,7 +7040,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": unknown; + "200": DevWebhookHealthResponse; "401": void; "404": void; "422": HTTPValidationError; @@ -13487,7 +13499,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; diff --git a/web/personas-open-source/src/lib/omiApi.generated.ts b/web/personas-open-source/src/lib/omiApi.generated.ts index c4e0c9f6490..1c3ca8e36d1 100644 --- a/web/personas-open-source/src/lib/omiApi.generated.ts +++ b/web/personas-open-source/src/lib/omiApi.generated.ts @@ -1371,6 +1371,17 @@ export interface DevApiKeyCreated { scopes?: Array | null; } +export interface DevWebhookHealthResponse { + disabled?: boolean; + failure_count?: number; + has_data: boolean; + last_error?: string | null; + last_failure_at?: number | null; + last_status?: number | null; + last_success_at?: number | null; + type: string; +} + export interface DeveloperActionItem { completed: boolean; completed_at?: string | null; @@ -3840,6 +3851,7 @@ export interface OmiApiSchemas { "DevApiKey": DevApiKey; "DevApiKeyCreate": DevApiKeyCreate; "DevApiKeyCreated": DevApiKeyCreated; + "DevWebhookHealthResponse": DevWebhookHealthResponse; "DeveloperActionItem": DeveloperActionItem; "DeveloperConversation": DeveloperConversation; "DeveloperConversationActionItem": DeveloperConversationActionItem; @@ -7028,7 +7040,7 @@ export interface OmiApiPaths { get: { operationId: "get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get"; responses: { - "200": unknown; + "200": DevWebhookHealthResponse; "401": void; "404": void; "422": HTTPValidationError; @@ -13487,7 +13499,7 @@ export async function enable_user_webhook_endpoint_v1_users_developer_webhook__w return _res.status === 204 ? (undefined as any) : await _res.json(); } -export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { +export async function get_user_webhook_health_endpoint_v1_users_developer_webhook__wtype__health_get(path: { wtype: WebhookType }, header: { authorization?: string, X_App_Platform?: string, X_Device_Id_Hash?: string, X_App_Version?: string }, init?: OmiApiClientInit): Promise { const _base = init?.baseURL ?? ""; const _path = `/v1/users/developer/webhook/${path.wtype}/health`; const _search = ""; From ecf9c686aab2a698c32e2d122fb8a2fc824ddc5d Mon Sep 17 00:00:00 2001 From: ZachL111 Date: Sat, 18 Jul 2026 23:53:55 -0700 Subject: [PATCH 7/7] Raise the routers/users.py ratchet baseline for the webhook health endpoint The endpoint grew routers/users.py past its frozen baseline. Raised to the measured line count with the required one-line justification rather than splitting a health read away from the developer-webhook handlers it shares auth and serialization with. --- .../backend-routers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/product_file_line_count_ratchet_baseline/backend-routers.json b/.github/scripts/product_file_line_count_ratchet_baseline/backend-routers.json index d87d5fef667..473688ec29a 100644 --- a/.github/scripts/product_file_line_count_ratchet_baseline/backend-routers.json +++ b/.github/scripts/product_file_line_count_ratchet_baseline/backend-routers.json @@ -5,11 +5,11 @@ "backend/routers/developer.py": 2184, "backend/routers/mcp_sse.py": 1857, "backend/routers/sync.py": 2024, - "backend/routers/users.py": 2046 + "backend/routers/users.py": 2093 }, "raise_justifications": { "backend/routers/chat.py": "PTT stereo rejection keeps the serving STT provider boundary explicit in the established chat admission owner.", - "backend/routers/users.py": "GET /v1/users/subscription serializes the new mobile plus/max plans as `unlimited` for clients whose plan enum predates them, so day-one buyers read as paid instead of Free (mirrors the existing operator remap); real limits/grandfather are computed from the true plan first.", + "backend/routers/users.py": "GET /v1/users/developer/webhook/{wtype}/health lives with the other developer-webhook routes it shares auth, uid scoping and response serialization with; a separate module would split one health read away from the handlers that own that surface.", "backend/routers/sync.py": "Fresh Sync admission and final Cloud Tasks ledger recovery retain their coupled task, lock, ledger, and staged-blob lifecycle in the existing ingestion owner rather than a new module." }, "threshold": 1500