ugly-app 0.1.118 → 0.1.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +273 -19
  2. package/coverage/client/AppProvider.tsx.html +1 -1
  3. package/coverage/client/FeedbackContext.ts.html +1 -1
  4. package/coverage/client/LoginPopup.tsx.html +1 -1
  5. package/coverage/client/Router.tsx.html +1 -1
  6. package/coverage/client/Screenshot.ts.html +1 -1
  7. package/coverage/client/ViewFlipper.tsx.html +1 -1
  8. package/coverage/client/animation/Animated.tsx.html +1 -1
  9. package/coverage/client/animation/animatedValue.ts.html +1 -1
  10. package/coverage/client/animation/index.html +1 -1
  11. package/coverage/client/audio/index.html +1 -1
  12. package/coverage/client/audio/useSTT.ts.html +2 -2
  13. package/coverage/client/audio/useTTS.ts.html +1 -1
  14. package/coverage/client/components/Button.tsx.html +1 -1
  15. package/coverage/client/components/FeedbackButton.tsx.html +1 -1
  16. package/coverage/client/components/Input.tsx.html +1 -1
  17. package/coverage/client/components/Modal.tsx.html +1 -1
  18. package/coverage/client/components/Text.tsx.html +1 -1
  19. package/coverage/client/components/Toast.tsx.html +1 -1
  20. package/coverage/client/components/index.html +1 -1
  21. package/coverage/client/components/zIndex.ts.html +1 -1
  22. package/coverage/client/createSocket.ts.html +48 -48
  23. package/coverage/client/index.html +1 -1
  24. package/coverage/clover.xml +561 -375
  25. package/coverage/coverage-final.json +22 -16
  26. package/coverage/index.html +42 -42
  27. package/coverage/server/Auth.ts.html +1 -1
  28. package/coverage/server/Cache.ts.html +1 -1
  29. package/coverage/server/DB.ts.html +1 -1
  30. package/coverage/server/Email.ts.html +1 -1
  31. package/coverage/server/EmailTemplate.ts.html +1 -1
  32. package/coverage/server/PushNotification.ts.html +1 -1
  33. package/coverage/server/RateLimit.ts.html +1 -1
  34. package/coverage/server/Router.ts.html +1 -1
  35. package/coverage/server/Socket.ts.html +1 -1
  36. package/coverage/server/Storage.ts.html +1 -1
  37. package/coverage/server/StoreHandlers.ts.html +1 -1
  38. package/coverage/server/ai/ImageGenClient.ts.html +1 -1
  39. package/coverage/server/ai/ProviderSelector.ts.html +44 -8
  40. package/coverage/server/ai/TextGenClient.ts.html +1 -1
  41. package/coverage/server/ai/WebSearchClient.ts.html +322 -0
  42. package/coverage/server/ai/fallbacks.ts.html +1 -1
  43. package/coverage/server/ai/index.html +29 -14
  44. package/coverage/server/ai/index.ts.html +31 -4
  45. package/coverage/server/ai/providers/Claude.ts.html +1 -1
  46. package/coverage/server/ai/providers/FAL.ts.html +1 -1
  47. package/coverage/server/ai/providers/Fireworks.ts.html +1 -1
  48. package/coverage/server/ai/providers/Google.ts.html +1 -1
  49. package/coverage/server/ai/providers/GoogleImage.ts.html +1 -1
  50. package/coverage/server/ai/providers/Groq.ts.html +1 -1
  51. package/coverage/server/ai/providers/Kagi.ts.html +505 -0
  52. package/coverage/server/ai/providers/Kie.ts.html +1 -1
  53. package/coverage/server/ai/providers/KieImage.ts.html +1 -1
  54. package/coverage/server/ai/providers/OpenAIText.ts.html +1 -1
  55. package/coverage/server/ai/providers/Together.ts.html +1 -1
  56. package/coverage/server/ai/providers/TogetherImage.ts.html +1 -1
  57. package/coverage/server/ai/providers/UglyBotImageGenProvider.ts.html +1 -1
  58. package/coverage/server/ai/providers/UglyBotTextGenProvider.ts.html +1 -1
  59. package/coverage/server/ai/providers/UglyBotWebSearchProvider.ts.html +412 -0
  60. package/coverage/server/ai/providers/Wavespeed.ts.html +2 -2
  61. package/coverage/server/ai/providers/index.html +39 -9
  62. package/coverage/server/ai/registry.ts.html +61 -10
  63. package/coverage/server/ai/types.ts.html +85 -4
  64. package/coverage/server/audio/STTStream.ts.html +1 -1
  65. package/coverage/server/audio/TTSStream.ts.html +1 -1
  66. package/coverage/server/audio/index.html +1 -1
  67. package/coverage/server/audio/index.ts.html +1 -1
  68. package/coverage/server/audio/resample.ts.html +1 -1
  69. package/coverage/server/audio/stt/GroqWhisper.ts.html +37 -4
  70. package/coverage/server/audio/stt/index.html +5 -5
  71. package/coverage/server/audio/stt/registry.ts.html +1 -1
  72. package/coverage/server/audio/tts/InWorld.ts.html +5 -2
  73. package/coverage/server/audio/tts/index.html +1 -1
  74. package/coverage/server/audio/tts/registry.ts.html +1 -1
  75. package/coverage/server/billing/BillingGateway.ts.html +273 -33
  76. package/coverage/server/billing/BillingLedger.ts.html +39 -39
  77. package/coverage/server/billing/CreditStore.ts.html +24 -24
  78. package/coverage/server/billing/LimitEnforcer.ts.html +57 -57
  79. package/coverage/server/billing/UserLimitCache.ts.html +27 -27
  80. package/coverage/server/billing/index.html +23 -23
  81. package/coverage/server/billing/types.ts.html +401 -11
  82. package/coverage/server/embeddings/EmbeddingClient.ts.html +1 -1
  83. package/coverage/server/embeddings/index.html +1 -1
  84. package/coverage/server/embeddings/providers/OpenAI.ts.html +1 -1
  85. package/coverage/server/embeddings/providers/index.html +1 -1
  86. package/coverage/server/embeddings/registry.ts.html +1 -1
  87. package/coverage/server/index.html +1 -1
  88. package/coverage/shared/Api.ts.html +1 -1
  89. package/coverage/shared/Audio.ts.html +1318 -0
  90. package/coverage/shared/DB.ts.html +1 -1
  91. package/coverage/shared/Errors.ts.html +1 -1
  92. package/coverage/shared/Experiment.ts.html +1 -1
  93. package/coverage/shared/ImageGen.ts.html +1 -1
  94. package/coverage/shared/Router.ts.html +1 -1
  95. package/coverage/shared/TextGen.ts.html +1 -1
  96. package/coverage/shared/Voice.ts.html +343 -0
  97. package/coverage/shared/WebSearch.ts.html +169 -0
  98. package/coverage/shared/index.html +53 -8
  99. package/coverage/shared/index.ts.html +141 -3
  100. package/dist/cli/version.d.ts +1 -1
  101. package/dist/cli/version.js +1 -1
  102. package/dist/client/audio/BlobAudioAnalyzer.js.map +1 -1
  103. package/dist/client/audio/CircleVisualizer.js.map +1 -1
  104. package/dist/client/audio/MicVisualizer.js.map +1 -1
  105. package/dist/client/audio/WaveVisualizer.js.map +1 -1
  106. package/dist/client/audio/blob-styles/MetaballBlobStyle.js.map +1 -1
  107. package/dist/client/audio/blob-styles/OrbsBlobStyle.js.map +1 -1
  108. package/dist/client/audio/blob-styles/OrganicBlobStyle.js.map +1 -1
  109. package/dist/client/audio/blob-styles/ParticleBlobStyle.js.map +1 -1
  110. package/dist/client/audio/blob-styles/SiriBlobStyle.js.map +1 -1
  111. package/dist/server/audio/stt/AudioStreamProcessor.js.map +1 -1
  112. package/dist/server/audio/stt/Deepgram.d.ts.map +1 -1
  113. package/dist/server/audio/stt/Deepgram.js +2 -1
  114. package/dist/server/audio/stt/Deepgram.js.map +1 -1
  115. package/dist/server/audio/stt/PyannoteSegmentation.js.map +1 -1
  116. package/dist/server/audio/stt/SileroVAD.js.map +1 -1
  117. package/dist/server/audio/tts/LipSync.js.map +1 -1
  118. package/dist/server/audio/tts/LipSyncJa.js.map +1 -1
  119. package/dist/server/audio/tts/LipSyncZh.js.map +1 -1
  120. package/dist/server/audio/tts/TextToSpeech.js.map +1 -1
  121. package/dist/server/audio/tts/TextToSpeechStream.js.map +1 -1
  122. package/dist/server/audio/voice/index.js.map +1 -1
  123. package/dist/shared/Audio.d.ts.map +1 -1
  124. package/dist/shared/Audio.js +6 -8
  125. package/dist/shared/Audio.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/version.ts +1 -1
  128. package/src/client/audio/BlobAudioAnalyzer.ts +2 -2
  129. package/src/client/audio/CircleVisualizer.ts +2 -2
  130. package/src/client/audio/MicVisualizer.tsx +3 -3
  131. package/src/client/audio/WaveVisualizer.ts +1 -1
  132. package/src/client/audio/blob-styles/MetaballBlobStyle.ts +8 -8
  133. package/src/client/audio/blob-styles/OrbsBlobStyle.ts +8 -8
  134. package/src/client/audio/blob-styles/OrganicBlobStyle.ts +8 -8
  135. package/src/client/audio/blob-styles/ParticleBlobStyle.ts +10 -10
  136. package/src/client/audio/blob-styles/SiriBlobStyle.ts +11 -11
  137. package/src/server/audio/stt/AudioStreamProcessor.ts +1 -1
  138. package/src/server/audio/stt/Deepgram.ts +5 -4
  139. package/src/server/audio/stt/PyannoteSegmentation.ts +1 -1
  140. package/src/server/audio/stt/SileroVAD.ts +1 -1
  141. package/src/server/audio/tts/LipSync.ts +9 -9
  142. package/src/server/audio/tts/LipSyncJa.ts +5 -5
  143. package/src/server/audio/tts/LipSyncZh.ts +3 -3
  144. package/src/server/audio/tts/TextToSpeech.ts +6 -6
  145. package/src/server/audio/tts/TextToSpeechStream.ts +6 -6
  146. package/src/server/audio/voice/index.ts +2 -2
  147. package/src/shared/Audio.ts +6 -10
package/README.md CHANGED
@@ -8,8 +8,10 @@ A full-stack TypeScript framework for building production-ready web applications
8
8
  - **Client**: React + Vite with typed routing, lazy pages, and popup management
9
9
  - **Database**: MongoDB with typed collections, dot-notation updates, indexes, migrations, and live document tracking
10
10
  - **Auth**: JWT + HttpOnly cookies, ugly.bot OAuth out of the box, extensible via `AuthProvider`
11
- - **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed) + embeddings + STT/TTS
11
+ - **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks, Kie) + image generation (Together, FAL, Google, Wavespeed, Kie) + embeddings + STT/TTS
12
12
  - **Storage**: Cloudflare R2 / AWS S3 with presigned uploads
13
+ - **Web search**: Kagi and UglyBot providers with search, summarize, and enrich
14
+ - **Billing**: Usage-based billing with per-user/global limits, credits, and threshold alerts
13
15
  - **CLI**: `ugly-app` commands for dev, build, deploy, migrations, logs, and auth utilities
14
16
 
15
17
  ## Quick start
@@ -454,7 +456,7 @@ import {
454
456
  SlideFromRight,
455
457
  } from 'ugly-app/client';
456
458
 
457
- // Create an animated value (01 spring)
459
+ // Create an animated value (0->1 spring)
458
460
  const spring = createAnimatedValue(0);
459
461
  spring.start(1, { duration: 300, easing: easingFunctions.easeOut });
460
462
 
@@ -489,6 +491,10 @@ const anim = useAnimatedValue(0);
489
491
 
490
492
  Available easings: `easingFunctions.linear`, `easeIn`, `easeOut`, `easeInOut`, `springGentle`, `springSnappy`, `springBouncy`, `slow`.
491
493
 
494
+ Additional animation hooks:
495
+ - `useAnimatedTransition` / `useAnimatedPresence` — mount/unmount transitions with phase tracking
496
+ - `useScrollAnimation` / `useStaggerAnimation` — scroll-driven and staggered entrance animations
497
+
492
498
  #### Screenshot capture
493
499
 
494
500
  ```typescript
@@ -501,7 +507,7 @@ const dataUrl = await captureScreenshot(); // captures the current viewport
501
507
 
502
508
  `ugly-app/client` exports a set of built-in UI components:
503
509
 
504
- `Button`, `Card`, `EnumInput`, `Header`, `Image`, `Input`, `Modal`, `PageLayout`, `Panel`, `PopupPanel`, `Pressable`, `ScrollView`, `SettingGroup`, `Text`, `Toast`, `View`
510
+ `Button`, `Card`, `EnumInput`, `Header`, `Image`, `Input`, `Modal`, `PageLayout`, `Panel`, `PopupPanel`, `Pressable`, `ScrollView`, `SettingGroup`, `Text`, `Toast`, `View`, `ResponsiveGrid`, `TabPicker`, `HeaderTabPicker`, `TabContent`, `TabContentAllActive`
505
511
 
506
512
  ---
507
513
 
@@ -707,17 +713,111 @@ const embeddings = createEmbeddingClient();
707
713
  const similarity = cosineSimilarity(vectorA, vectorB);
708
714
  ```
709
715
 
710
- ### Speech-to-text / Text-to-speech
716
+ ### Speech-to-text (STT)
717
+
718
+ Server-side STT uses a provider registry pattern. Providers auto-register when their API key env var is set.
719
+
720
+ ```typescript
721
+ import { registerSTTProvider, selectSTTProvider, getAllSTTProviders } from 'ugly-app';
722
+ ```
723
+
724
+ **Built-in STT providers:**
725
+
726
+ | Provider | Import | Env var | Mode |
727
+ |----------|--------|---------|------|
728
+ | Deepgram (nova-2) | `deepgramSTTProvider` | `DEEPGRAM_API_KEY` | Real-time streaming via WebSocket |
729
+ | OpenAI Whisper | `openAIWhisperSTTProvider` | `OPENAI_API_KEY` | Batch (buffers audio, transcribes on stop) |
730
+ | Groq Whisper | `groqWhisperSTTProvider` | `GROQ_API_KEY` | Batch |
731
+
732
+ **STT provider interface:**
733
+
734
+ ```typescript
735
+ interface STTProvider {
736
+ name: string;
737
+ apiKeyEnv: string;
738
+ connect(
739
+ onTranscript: (result: STTTranscript) => void,
740
+ onError: (err: string) => void,
741
+ lang?: string,
742
+ options?: STTConnectOptions,
743
+ onUsage?: (report: STTUsageReport) => void,
744
+ ): Promise<STTSession>;
745
+ }
746
+
747
+ interface STTSession {
748
+ sendAudio(pcm16: Buffer): void; // PCM16 at 16kHz mono
749
+ stop(): Promise<void>;
750
+ }
751
+
752
+ interface STTTranscript { text: string; isFinal: boolean; lang?: string; words?: STTWord[] }
753
+ ```
754
+
755
+ **Client-side hook:**
756
+
757
+ ```typescript
758
+ import { useSTT } from 'ugly-app/client';
759
+ const { start, stop, transcript, isListening } = useSTT(socket, options);
760
+ ```
761
+
762
+ ### Text-to-speech (TTS)
763
+
764
+ ```typescript
765
+ import { registerTTSProvider, selectTTSProvider, azureTTSProvider } from 'ugly-app';
766
+ ```
767
+
768
+ **Built-in TTS provider:**
769
+
770
+ | Provider | Import | Env vars |
771
+ |----------|--------|----------|
772
+ | Azure TTS | `azureTTSProvider` | `AZURE_TTS_KEY`, `AZURE_TTS_REGION` |
773
+
774
+ **TTS provider interface:**
775
+
776
+ ```typescript
777
+ interface TTSProvider {
778
+ name: string;
779
+ apiKeyEnv: string;
780
+ stream(text: string, voice: string, options?: TTSStreamOptions): AsyncGenerator<TTSChunk>;
781
+ }
782
+
783
+ interface TTSChunk {
784
+ audio: Buffer; // PCM16, mono, 24kHz
785
+ word?: string;
786
+ startMs?: number;
787
+ durationMs?: number;
788
+ visemes?: TTSViseme[]; // When requestVisemes=true (for lip sync)
789
+ }
790
+ ```
791
+
792
+ **Client-side hook:**
793
+
794
+ ```typescript
795
+ import { useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
796
+ ```
797
+
798
+ ### Web search
711
799
 
712
800
  ```typescript
713
- // Server-side providers
714
- import { registerSTTProvider, groqWhisperSTTProvider, loadVADModel } from 'ugly-app';
715
- import { registerTTSProvider, azureTTSProvider } from 'ugly-app';
801
+ import { createWebSearchClient, registerWebSearchProvider } from 'ugly-app';
802
+ const search = createWebSearchClient(userId);
716
803
 
717
- // Client-side hooks
718
- import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
804
+ const results = await search.search({ query: 'hello', limit: 10 });
805
+ const summary = await search.summarize({ url: 'https://...' });
806
+ const web = await search.enrichWeb({ query: 'topic' });
807
+ const news = await search.enrichNews({ query: 'topic' });
719
808
  ```
720
809
 
810
+ **`WebSearchClient` methods:**
811
+
812
+ | Method | Description |
813
+ |--------|-------------|
814
+ | `search({ query, limit? })` | Web search — returns `{ items, related? }` |
815
+ | `summarize({ url?, text? })` | Summarize a URL or text — returns summary string |
816
+ | `enrichWeb({ query })` | Enriched web results |
817
+ | `enrichNews({ query })` | Enriched news results |
818
+
819
+ Built-in providers: Kagi (`KAGI_API_KEY`) and UglyBot.
820
+
721
821
  ### Custom providers
722
822
 
723
823
  ```typescript
@@ -777,6 +877,135 @@ Static build-time assets go in `client/public/`. Never hardcode `/asset/...` pat
777
877
 
778
878
  ---
779
879
 
880
+ ## Billing
881
+
882
+ Usage-based billing with per-user limits, global provider limits, pre-paid credits, and threshold alerts.
883
+
884
+ ```typescript
885
+ import { initBillingGateway, getBillingGateway } from 'ugly-app';
886
+
887
+ const billing = initBillingGateway(db, {
888
+ global: { hourlyUsd: 100, thresholdPct: 0.8 },
889
+ providers: {
890
+ openai: { hourlyUsd: 50, thresholdPct: 0.9 },
891
+ },
892
+ profitMarginPct: 0.2, // 20% markup on costs
893
+ });
894
+
895
+ // Charge a user
896
+ await billing.charge({
897
+ userId: '...',
898
+ provider: 'openai',
899
+ model: 'gpt-4o',
900
+ type: 'textGen', // 'textGen' | 'imageGen' | 'stt' | 'tts' | 'embedding' | 'service'
901
+ costUsd: 0.03,
902
+ inputTokens: 1000,
903
+ outputTokens: 500,
904
+ });
905
+
906
+ // Pre-flight check
907
+ const canPay = await billing.canCharge(userId, 0.05);
908
+
909
+ // Grant credits
910
+ await billing.grantCredit(userId, 10.00);
911
+
912
+ // Query spend
913
+ const usage = await billing.getSpend(userId, { from, to });
914
+
915
+ // Threshold callbacks
916
+ billing.setUserThresholdCallback((userId, period, spend, limit) => { /* alert */ });
917
+ billing.setGlobalThresholdCallback((period, spend, limit) => { /* alert */ });
918
+ ```
919
+
920
+ **Per-user limits** are resolved via `setUserLimitHook()`:
921
+
922
+ ```typescript
923
+ billing.setUserLimitHook(async (userId) => ({
924
+ hourlyUsd: 5,
925
+ dailyUsd: 50,
926
+ weeklyUsd: 200,
927
+ thresholds: { hourly: 0.8, daily: 0.9, weekly: 0.95 },
928
+ }));
929
+ ```
930
+
931
+ The billing state machine tracks spend across hourly, daily, and weekly windows. Global and per-provider limits are always enforced; user credits act as a fallback when user limits are exceeded but never bypass global limits.
932
+
933
+ ---
934
+
935
+ ## Experiments
936
+
937
+ A/B testing with deterministic user bucketing and event tracking.
938
+
939
+ ```typescript
940
+ // shared/experiments.ts
941
+ import type { Experiment } from 'ugly-app/shared';
942
+
943
+ export const experiments: Experiment[] = [
944
+ {
945
+ id: 'new-onboarding',
946
+ name: 'New Onboarding Flow',
947
+ description: 'Test the redesigned onboarding',
948
+ active: true,
949
+ branches: [
950
+ { id: 'control', name: 'Control', weight: 50 },
951
+ { id: 'variant', name: 'Variant', weight: 50 },
952
+ ],
953
+ events: ['ONBOARDING_COMPLETE', 'ONBOARDING_SKIP'],
954
+ },
955
+ ];
956
+ ```
957
+
958
+ **Server-side assignment:**
959
+
960
+ ```typescript
961
+ import { getExperimentAssignments, getExperimentBranch } from 'ugly-app';
962
+
963
+ // Get all active experiment assignments for a user
964
+ const branches = getExperimentAssignments(userId, sessionId, experiments);
965
+ // => { 'new-onboarding': 'variant' }
966
+
967
+ // Get a single experiment branch
968
+ const branch = getExperimentBranch(experiment, userId, sessionId);
969
+ ```
970
+
971
+ Bucketing uses a deterministic hash of `experimentId:userId` (or `sessionId` if no user), so the same user always gets the same branch. Weights control the relative distribution across branches.
972
+
973
+ ---
974
+
975
+ ## Event logging
976
+
977
+ Server-side event capture for analytics, tied to experiments.
978
+
979
+ ```typescript
980
+ import { eventLogCapture, eventLogServerCapture } from 'ugly-app';
981
+
982
+ // Capture with returned eventId
983
+ const { eventId } = await eventLogCapture({
984
+ eventName: 'BUTTON_CLICK',
985
+ sessionId,
986
+ userId,
987
+ properties: { page: 'home' },
988
+ experimentBranches: branches,
989
+ }, userId);
990
+
991
+ // Fire-and-forget server capture
992
+ await eventLogServerCapture('SESSION_START', { source: 'web' }, sessionId, userId, branches);
993
+ ```
994
+
995
+ **Query functions:**
996
+
997
+ | Function | Description |
998
+ |----------|-------------|
999
+ | `eventLogGetList(input)` | Paginated event list (cursor, date range, filters) |
1000
+ | `eventLogGetTopUsers(input)` | Top users by event count |
1001
+ | `eventLogGetTopSessions(input)` | Top sessions by event count |
1002
+ | `eventLogGetTopEvents(input)` | Top events by frequency |
1003
+ | `eventLogGetCounts(input)` | Time-series counts (granularity: `'seconds'` \| `'minutes'` \| `'days'`) |
1004
+ | `eventLogGetUniqueUsersCounts(input)` | Unique users per time interval |
1005
+ | `eventLogGetUniqueSessionsCounts(input)` | Unique sessions per time interval |
1006
+
1007
+ ---
1008
+
780
1009
  ## Additional server APIs
781
1010
 
782
1011
  ### Email
@@ -816,12 +1045,31 @@ const redis = getRedisClient();
816
1045
  ### Worker queues
817
1046
 
818
1047
  ```typescript
819
- import { createWorkerQueue, enqueueTask } from 'ugly-app';
1048
+ import { createWorkerQueue } from 'ugly-app';
1049
+
1050
+ const queue = createWorkerQueue({
1051
+ streamName: 'JOBS', // NATS stream name (default: 'JOBS')
1052
+ concurrency: 10, // max concurrent jobs (default: 10)
1053
+ maxRetries: 3, // max retry attempts (default: 3)
1054
+ clockServerOnly: true, // only process if IS_CLOCK_SERVER=true (default: true)
1055
+ });
820
1056
 
821
- const queue = createWorkerQueue({ /* config */ });
822
- configurator.setWorkerQueue(queue);
1057
+ queue.registerHandler<MyPayload>('sendEmail', async (job) => {
1058
+ job.working(); // extend ack deadline for long-running jobs
1059
+ await doWork(job.payload);
1060
+ });
1061
+
1062
+ await queue.enqueue('sendEmail', { to: 'user@example.com' }, { delay: 5000 });
1063
+
1064
+ configurator.setWorkerQueue(queue); // register with the app for lifecycle management
1065
+ ```
1066
+
1067
+ **Scheduled tasks** — prevent duplicate enqueuing via MongoDB upsert:
1068
+
1069
+ ```typescript
1070
+ import { enqueueTask } from 'ugly-app';
823
1071
 
824
- await enqueueTask('taskName', payload);
1072
+ await enqueueTask(taskDoc, queue, { delay: 60000 });
825
1073
  ```
826
1074
 
827
1075
  ### Billing
@@ -938,9 +1186,9 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
938
1186
 
939
1187
  | Import path | Description |
940
1188
  |-------------|-------------|
941
- | `ugly-app` | Server APIs (createApp, DB, auth, AI, email, storage, etc.) |
942
- | `ugly-app/shared` | Shared types and utilities (defineRequests, defineCollections, definePage, Zod, etc.) |
943
- | `ugly-app/client` | Client APIs (createSocket, createRouter, AppProvider, components, animations, etc.) |
1189
+ | `ugly-app` | Server APIs (createApp, DB, auth, AI, email, storage, billing, worker queues, etc.) |
1190
+ | `ugly-app/shared` | Shared types and utilities (defineRequests, defineCollections, definePage, experiments, Zod, etc.) |
1191
+ | `ugly-app/client` | Client APIs (createSocket, createRouter, AppProvider, components, animations, audio, etc.) |
944
1192
  | `ugly-app/playwright` | Playwright test utilities |
945
1193
  | `ugly-app/webrtc` | WebRTC utilities |
946
1194
 
@@ -958,6 +1206,8 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
958
1206
  | `UGLY_BOT_TOKEN` | App token for AI proxy (`/ai/request`) |
959
1207
  | `REDIS_URL` | Redis (optional, in-memory fallback for dev) |
960
1208
  | `NATS_URL` | NATS server URL |
1209
+ | `CLOCK_ENABLED` | Set to `true` to enable `setOnMinuteTick`/`setOnHourlyTick` handlers |
1210
+ | `IS_CLOCK_SERVER` | Set to `true` on the instance that should process delayed worker queue jobs |
961
1211
  | `STORAGE_ACCOUNT_ID` | Cloudflare R2 account ID |
962
1212
  | `STORAGE_ACCESS_KEY_ID` | R2 access key |
963
1213
  | `STORAGE_SECRET_ACCESS_KEY` | R2 secret key |
@@ -968,12 +1218,16 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
968
1218
  | `ANTHROPIC_API_KEY` | Anthropic Claude key |
969
1219
  | `OPENAI_API_KEY` | OpenAI key |
970
1220
  | `GOOGLE_API_KEY` | Google Gemini key |
971
- | `MAILGUN_API_KEY` | Mailgun key |
972
- | `MAILGUN_DOMAIN` | Mailgun sending domain |
1221
+ | `GROQ_API_KEY` | Groq key |
973
1222
  | `KIE_API_KEY` | Kie.ai key |
974
1223
  | `KIE_BASE_URL` | Kie.ai base URL override (optional) |
1224
+ | `DEEPGRAM_API_KEY` | Deepgram STT key |
1225
+ | `AZURE_TTS_KEY` | Azure TTS key |
1226
+ | `AZURE_TTS_REGION` | Azure TTS region |
1227
+ | `KAGI_API_KEY` | Kagi web search key |
1228
+ | `MAILGUN_API_KEY` | Mailgun key |
1229
+ | `MAILGUN_DOMAIN` | Mailgun sending domain |
975
1230
  | `MAILGUN_FROM` | Default from address |
976
- | `CLOCK_ENABLED` | Set to `true` to enable `setOnMinuteTick`/`setOnHourlyTick` handlers |
977
1231
 
978
1232
  Client-side variables must be prefixed with `VITE_`.
979
1233
 
@@ -787,7 +787,7 @@ function PopupLayer({ popups }: { popups: PopupEntry[] }) {
787
787
  <div class='footer quiet pad2 space-top1 center small'>
788
788
  Code coverage generated by
789
789
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
790
- at 2026-03-30T07:32:37.770Z
790
+ at 2026-03-30T16:02:56.403Z
791
791
  </div>
792
792
  <script src="../prettify.js"></script>
793
793
  <script>
@@ -118,7 +118,7 @@ export function clearFeedbackContext(): void {
118
118
  <div class='footer quiet pad2 space-top1 center small'>
119
119
  Code coverage generated by
120
120
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
121
- at 2026-03-30T07:32:37.770Z
121
+ at 2026-03-30T16:02:56.403Z
122
122
  </div>
123
123
  <script src="../prettify.js"></script>
124
124
  <script>
@@ -310,7 +310,7 @@ export function LoginPopup({ onSuccess, inline = false }: LoginPopupProps) {
310
310
  <div class='footer quiet pad2 space-top1 center small'>
311
311
  Code coverage generated by
312
312
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
313
- at 2026-03-30T07:32:37.770Z
313
+ at 2026-03-30T16:02:56.403Z
314
314
  </div>
315
315
  <script src="../prettify.js"></script>
316
316
  <script>
@@ -1918,7 +1918,7 @@ export function Link&lt;
1918
1918
  <div class='footer quiet pad2 space-top1 center small'>
1919
1919
  Code coverage generated by
1920
1920
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
1921
- at 2026-03-30T07:32:37.770Z
1921
+ at 2026-03-30T16:02:56.403Z
1922
1922
  </div>
1923
1923
  <script src="../prettify.js"></script>
1924
1924
  <script>
@@ -244,7 +244,7 @@ function blobToDataUrl(blob: Blob): Promise&lt;string&gt; {
244
244
  <div class='footer quiet pad2 space-top1 center small'>
245
245
  Code coverage generated by
246
246
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
247
- at 2026-03-30T07:32:37.770Z
247
+ at 2026-03-30T16:02:56.403Z
248
248
  </div>
249
249
  <script src="../prettify.js"></script>
250
250
  <script>
@@ -763,7 +763,7 @@ export function ViewFlipper({
763
763
  <div class='footer quiet pad2 space-top1 center small'>
764
764
  Code coverage generated by
765
765
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
766
- at 2026-03-30T07:32:37.770Z
766
+ at 2026-03-30T16:02:56.403Z
767
767
  </div>
768
768
  <script src="../prettify.js"></script>
769
769
  <script>
@@ -424,7 +424,7 @@ export const Animated = {
424
424
  <div class='footer quiet pad2 space-top1 center small'>
425
425
  Code coverage generated by
426
426
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
427
- at 2026-03-30T07:32:37.770Z
427
+ at 2026-03-30T16:02:56.403Z
428
428
  </div>
429
429
  <script src="../../prettify.js"></script>
430
430
  <script>
@@ -652,7 +652,7 @@ export function useAnimatedValueTracker(animatedValue: AnimatedValueRef): number
652
652
  <div class='footer quiet pad2 space-top1 center small'>
653
653
  Code coverage generated by
654
654
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
655
- at 2026-03-30T07:32:37.770Z
655
+ at 2026-03-30T16:02:56.403Z
656
656
  </div>
657
657
  <script src="../../prettify.js"></script>
658
658
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-03-30T07:32:37.770Z
119
+ at 2026-03-30T16:02:56.403Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -116,7 +116,7 @@
116
116
  <div class='footer quiet pad2 space-top1 center small'>
117
117
  Code coverage generated by
118
118
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
119
- at 2026-03-30T07:32:37.770Z
119
+ at 2026-03-30T16:02:56.403Z
120
120
  </div>
121
121
  <script src="../../prettify.js"></script>
122
122
  <script>
@@ -269,7 +269,7 @@
269
269
  <span class="cline-any cline-yes">19x</span>
270
270
  <span class="cline-any cline-yes">18x</span>
271
271
  <span class="cline-any cline-yes">18x</span>
272
- <span class="cline-any cline-yes">258x</span>
272
+ <span class="cline-any cline-yes">265x</span>
273
273
  <span class="cline-any cline-yes">4x</span>
274
274
  <span class="cline-any cline-yes">4x</span>
275
275
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -433,7 +433,7 @@ export function useSTT(socket: SocketLike, options: STTOptions = {}) {
433
433
  <div class='footer quiet pad2 space-top1 center small'>
434
434
  Code coverage generated by
435
435
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
436
- at 2026-03-30T07:32:37.770Z
436
+ at 2026-03-30T16:02:56.403Z
437
437
  </div>
438
438
  <script src="../../prettify.js"></script>
439
439
  <script>
@@ -367,7 +367,7 @@ export function useTTS(socket: SocketLike) {
367
367
  <div class='footer quiet pad2 space-top1 center small'>
368
368
  Code coverage generated by
369
369
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
370
- at 2026-03-30T07:32:37.770Z
370
+ at 2026-03-30T16:02:56.403Z
371
371
  </div>
372
372
  <script src="../../prettify.js"></script>
373
373
  <script>
@@ -346,7 +346,7 @@ export function Button({
346
346
  <div class='footer quiet pad2 space-top1 center small'>
347
347
  Code coverage generated by
348
348
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
349
- at 2026-03-30T07:32:37.770Z
349
+ at 2026-03-30T16:02:56.403Z
350
350
  </div>
351
351
  <script src="../../prettify.js"></script>
352
352
  <script>
@@ -604,7 +604,7 @@ export function FeedbackButton({ socket }: Props): ReactNode {
604
604
  <div class='footer quiet pad2 space-top1 center small'>
605
605
  Code coverage generated by
606
606
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
607
- at 2026-03-30T07:32:37.770Z
607
+ at 2026-03-30T16:02:56.403Z
608
608
  </div>
609
609
  <script src="../../prettify.js"></script>
610
610
  <script>
@@ -262,7 +262,7 @@ export function Input({
262
262
  <div class='footer quiet pad2 space-top1 center small'>
263
263
  Code coverage generated by
264
264
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
265
- at 2026-03-30T07:32:37.770Z
265
+ at 2026-03-30T16:02:56.403Z
266
266
  </div>
267
267
  <script src="../../prettify.js"></script>
268
268
  <script>
@@ -244,7 +244,7 @@ export function Modal({
244
244
  <div class='footer quiet pad2 space-top1 center small'>
245
245
  Code coverage generated by
246
246
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
247
- at 2026-03-30T07:32:37.770Z
247
+ at 2026-03-30T16:02:56.403Z
248
248
  </div>
249
249
  <script src="../../prettify.js"></script>
250
250
  <script>
@@ -334,7 +334,7 @@ export function Text({
334
334
  <div class='footer quiet pad2 space-top1 center small'>
335
335
  Code coverage generated by
336
336
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
337
- at 2026-03-30T07:32:37.770Z
337
+ at 2026-03-30T16:02:56.403Z
338
338
  </div>
339
339
  <script src="../../prettify.js"></script>
340
340
  <script>
@@ -220,7 +220,7 @@ export function Toast({
220
220
  <div class='footer quiet pad2 space-top1 center small'>
221
221
  Code coverage generated by
222
222
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
223
- at 2026-03-30T07:32:37.770Z
223
+ at 2026-03-30T16:02:56.403Z
224
224
  </div>
225
225
  <script src="../../prettify.js"></script>
226
226
  <script>
@@ -191,7 +191,7 @@
191
191
  <div class='footer quiet pad2 space-top1 center small'>
192
192
  Code coverage generated by
193
193
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
194
- at 2026-03-30T07:32:37.770Z
194
+ at 2026-03-30T16:02:56.403Z
195
195
  </div>
196
196
  <script src="../../prettify.js"></script>
197
197
  <script>
@@ -103,7 +103,7 @@ export const Z = {
103
103
  <div class='footer quiet pad2 space-top1 center small'>
104
104
  Code coverage generated by
105
105
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
106
- at 2026-03-30T07:32:37.770Z
106
+ at 2026-03-30T16:02:56.403Z
107
107
  </div>
108
108
  <script src="../../prettify.js"></script>
109
109
  <script>