forge-openclaw-plugin 0.2.25 → 0.2.27

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 (237) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-VmF4FAfr.js → board-C6jCchjI.js} +3 -3
  3. package/dist/assets/{board-VmF4FAfr.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-DvkU14p-.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-DvkU14p-.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-DgiPof9E.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-DgiPof9E.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-nYfoC0Gq.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-nYfoC0Gq.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -8
  18. package/dist/openclaw/local-runtime.d.ts +3 -1
  19. package/dist/openclaw/local-runtime.js +67 -15
  20. package/dist/openclaw/plugin-entry-shared.js +24 -2
  21. package/dist/openclaw/plugin-sdk-types.d.ts +17 -0
  22. package/dist/openclaw/routes.d.ts +27 -0
  23. package/dist/openclaw/routes.js +16 -12
  24. package/dist/openclaw/tools.js +0 -3
  25. package/dist/server/server/migrations/001_core.sql +411 -0
  26. package/dist/server/server/migrations/002_psyche.sql +392 -0
  27. package/dist/server/server/migrations/003_habits.sql +30 -0
  28. package/dist/server/server/migrations/004_habit_links.sql +8 -0
  29. package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
  30. package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
  31. package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
  32. package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
  33. package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
  34. package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
  35. package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  36. package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
  37. package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  38. package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  39. package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
  40. package/dist/server/server/migrations/016_health_companion.sql +158 -0
  41. package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  42. package/dist/server/server/migrations/017_preferences.sql +131 -0
  43. package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
  44. package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
  45. package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  46. package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  47. package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
  48. package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
  49. package/dist/server/server/migrations/024_questionnaires.sql +96 -0
  50. package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
  51. package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
  52. package/dist/server/server/migrations/027_ai_processors.sql +31 -0
  53. package/dist/server/server/migrations/028_movement_domain.sql +136 -0
  54. package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
  55. package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
  56. package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  57. package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
  58. package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
  59. package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
  60. package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
  61. package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  62. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  63. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  64. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  65. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  66. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  67. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  68. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  69. package/dist/server/{app.js → server/src/app.js} +2112 -414
  70. package/dist/server/server/src/connectors/box-registry.js +223 -0
  71. package/dist/server/server/src/data-management-types.js +107 -0
  72. package/dist/server/{db.js → server/src/db.js} +72 -4
  73. package/dist/server/server/src/debug.js +19 -0
  74. package/dist/server/{demo-data.js → server/src/demo-data.js} +2 -2
  75. package/dist/server/{health.js → server/src/health.js} +702 -18
  76. package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +7 -4
  77. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  78. package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +18 -1
  79. package/dist/server/{managers → server/src/managers}/runtime.js +9 -0
  80. package/dist/server/{movement.js → server/src/movement.js} +1971 -112
  81. package/dist/server/{openapi.js → server/src/openapi.js} +491 -3
  82. package/dist/server/{psyche-types.js → server/src/psyche-types.js} +9 -1
  83. package/dist/server/{repositories → server/src/repositories}/activity-events.js +8 -0
  84. package/dist/server/{repositories → server/src/repositories}/ai-connectors.js +758 -47
  85. package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
  86. package/dist/server/{repositories → server/src/repositories}/habits.js +37 -1
  87. package/dist/server/{repositories → server/src/repositories}/model-settings.js +13 -3
  88. package/dist/server/{repositories → server/src/repositories}/notes.js +3 -0
  89. package/dist/server/{repositories → server/src/repositories}/settings.js +431 -21
  90. package/dist/server/{repositories → server/src/repositories}/tasks.js +170 -10
  91. package/dist/server/server/src/runtime-data-root.js +82 -0
  92. package/dist/server/server/src/screen-time.js +802 -0
  93. package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
  94. package/dist/server/server/src/services/data-management.js +788 -0
  95. package/dist/server/{services → server/src/services}/entity-crud.js +205 -2
  96. package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
  97. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  98. package/dist/server/server/src/services/life-force-model.js +197 -0
  99. package/dist/server/server/src/services/life-force.js +1270 -0
  100. package/dist/server/server/src/services/psyche-observation-calendar.js +413 -0
  101. package/dist/server/{types.js → server/src/types.js} +420 -29
  102. package/dist/server/server/src/web.js +332 -0
  103. package/dist/server/src/components/customization/utility-widgets.js +439 -0
  104. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  105. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  106. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  107. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  108. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +147 -0
  109. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  110. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +136 -0
  111. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +47 -0
  112. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +132 -0
  113. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  114. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  115. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +62 -0
  116. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  117. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  118. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  119. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +6 -0
  120. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +49 -0
  121. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  122. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  123. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +78 -0
  124. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  125. package/dist/server/src/lib/api-error.js +37 -0
  126. package/dist/server/src/lib/api.js +2118 -0
  127. package/dist/server/src/lib/calendar-name-deduper.js +144 -0
  128. package/dist/server/src/lib/data-management-types.js +1 -0
  129. package/dist/server/src/lib/diagnostics.js +67 -0
  130. package/dist/server/src/lib/entity-visuals.js +279 -0
  131. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  132. package/dist/server/src/lib/knowledge-graph.js +470 -0
  133. package/dist/server/src/lib/psyche-types.js +1 -0
  134. package/dist/server/src/lib/questionnaire-types.js +1 -0
  135. package/dist/server/src/lib/runtime-paths.js +24 -0
  136. package/dist/server/src/lib/schemas.js +238 -0
  137. package/dist/server/src/lib/snapshot-normalizer.js +416 -0
  138. package/dist/server/src/lib/theme-system.js +319 -0
  139. package/dist/server/src/lib/types.js +1 -0
  140. package/dist/server/src/lib/utils.js +22 -0
  141. package/dist/server/src/lib/workbench/boxes.js +16 -0
  142. package/dist/server/src/lib/workbench/contracts.js +229 -0
  143. package/dist/server/src/lib/workbench/nodes.js +215 -0
  144. package/dist/server/src/lib/workbench/registry.js +120 -0
  145. package/dist/server/src/lib/workbench/runtime.js +397 -0
  146. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  147. package/openclaw.plugin.json +1 -1
  148. package/package.json +1 -1
  149. package/server/index.js +68 -0
  150. package/server/migrations/035_google_local_auth_settings.sql +2 -0
  151. package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  152. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  153. package/server/migrations/038_data_management_settings.sql +11 -0
  154. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  155. package/server/migrations/040_screen_time_domain.sql +89 -0
  156. package/server/migrations/041_companion_source_states.sql +21 -0
  157. package/server/migrations/042_movement_boxes.sql +47 -0
  158. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  159. package/skills/forge-openclaw/SKILL.md +27 -11
  160. package/skills/forge-openclaw/entity_conversation_playbooks.md +411 -46
  161. package/skills/forge-openclaw/psyche_entity_playbooks.md +195 -20
  162. package/dist/assets/index-CFCKDIMH.js +0 -67
  163. package/dist/assets/index-CFCKDIMH.js.map +0 -1
  164. package/dist/assets/index-ZPY6U1TU.css +0 -1
  165. package/dist/assets/vendor-D9PTEPSB.js +0 -824
  166. package/dist/assets/vendor-D9PTEPSB.js.map +0 -1
  167. package/dist/assets/viz-Cqb6s--o.js +0 -34
  168. package/dist/assets/viz-Cqb6s--o.js.map +0 -1
  169. package/dist/server/connectors/box-registry.js +0 -257
  170. package/dist/server/services/psyche-observation-calendar.js +0 -46
  171. package/dist/server/web.js +0 -98
  172. /package/dist/server/{discovery-advertiser.js → server/src/discovery-advertiser.js} +0 -0
  173. /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
  174. /package/dist/server/{errors.js → server/src/errors.js} +0 -0
  175. /package/dist/server/{index.js → server/src/index.js} +0 -0
  176. /package/dist/server/{managers → server/src/managers}/base.js +0 -0
  177. /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
  178. /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
  179. /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
  180. /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
  181. /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
  182. /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
  183. /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
  184. /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
  185. /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
  186. /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
  187. /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
  188. /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
  189. /package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +0 -0
  190. /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
  191. /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
  192. /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
  193. /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
  194. /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
  195. /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
  196. /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
  197. /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
  198. /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
  199. /package/dist/server/{questionnaire-flow.js → server/src/questionnaire-flow.js} +0 -0
  200. /package/dist/server/{questionnaire-seeds.js → server/src/questionnaire-seeds.js} +0 -0
  201. /package/dist/server/{questionnaire-types.js → server/src/questionnaire-types.js} +0 -0
  202. /package/dist/server/{repositories → server/src/repositories}/ai-processors.js +0 -0
  203. /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
  204. /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
  205. /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
  206. /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
  207. /package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +0 -0
  208. /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
  209. /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
  210. /package/dist/server/{repositories → server/src/repositories}/preferences.js +0 -0
  211. /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
  212. /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
  213. /package/dist/server/{repositories → server/src/repositories}/questionnaires.js +0 -0
  214. /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
  215. /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
  216. /package/dist/server/{repositories → server/src/repositories}/surface-layouts.js +0 -0
  217. /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
  218. /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
  219. /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
  220. /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
  221. /package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +0 -0
  222. /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
  223. /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
  224. /package/dist/server/{services → server/src/services}/context.js +0 -0
  225. /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
  226. /package/dist/server/{services → server/src/services}/gamification.js +0 -0
  227. /package/dist/server/{services → server/src/services}/insights.js +0 -0
  228. /package/dist/server/{services → server/src/services}/openai-codex-oauth.js +0 -0
  229. /package/dist/server/{services → server/src/services}/projects.js +0 -0
  230. /package/dist/server/{services → server/src/services}/psyche.js +0 -0
  231. /package/dist/server/{services → server/src/services}/relations.js +0 -0
  232. /package/dist/server/{services → server/src/services}/reviews.js +0 -0
  233. /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
  234. /package/dist/server/{services → server/src/services}/tagging.js +0 -0
  235. /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
  236. /package/dist/server/{services → server/src/services}/work-time.js +0 -0
  237. /package/dist/server/{watch-mobile.js → server/src/watch-mobile.js} +0 -0
@@ -0,0 +1,802 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { getDatabase } from "./db.js";
4
+ import { recordDiagnosticLog } from "./repositories/diagnostic-logs.js";
5
+ import { getDefaultUser } from "./repositories/users.js";
6
+ const screenTimeAuthorizationStatusSchema = z.enum([
7
+ "not_determined",
8
+ "denied",
9
+ "approved",
10
+ "unavailable"
11
+ ]);
12
+ const screenTimeCaptureStateSchema = z.enum([
13
+ "disabled",
14
+ "capturing",
15
+ "waiting_for_snapshot",
16
+ "ready",
17
+ "sync_paused",
18
+ "unavailable",
19
+ "needs_authorization"
20
+ ]);
21
+ const screenTimeCaptureFreshnessSchema = z.enum([
22
+ "empty",
23
+ "fresh",
24
+ "stale",
25
+ "unavailable"
26
+ ]);
27
+ const screenTimeAppUsageInputSchema = z.object({
28
+ bundleIdentifier: z.string().trim().min(1),
29
+ displayName: z.string().trim().default(""),
30
+ categoryLabel: z.string().trim().nullable().default(null),
31
+ totalActivitySeconds: z.number().int().nonnegative().default(0),
32
+ pickupCount: z.number().int().nonnegative().default(0),
33
+ notificationCount: z.number().int().nonnegative().default(0)
34
+ });
35
+ const screenTimeCategoryUsageInputSchema = z.object({
36
+ categoryLabel: z.string().trim().min(1),
37
+ totalActivitySeconds: z.number().int().nonnegative().default(0)
38
+ });
39
+ const screenTimeDaySummaryInputSchema = z.object({
40
+ dateKey: z.string().trim().min(1),
41
+ totalActivitySeconds: z.number().int().nonnegative().default(0),
42
+ pickupCount: z.number().int().nonnegative().default(0),
43
+ notificationCount: z.number().int().nonnegative().default(0),
44
+ firstPickupAt: z.string().datetime().nullable().default(null),
45
+ longestActivitySeconds: z.number().int().nonnegative().default(0),
46
+ topAppBundleIdentifiers: z.array(z.string().trim().min(1)).default([]),
47
+ topCategoryLabels: z.array(z.string().trim().min(1)).default([]),
48
+ metadata: z.record(z.string(), z.unknown()).default({})
49
+ });
50
+ const screenTimeHourlySegmentInputSchema = z.object({
51
+ dateKey: z.string().trim().min(1),
52
+ hourIndex: z.number().int().min(0).max(23),
53
+ startedAt: z.string().datetime(),
54
+ endedAt: z.string().datetime(),
55
+ totalActivitySeconds: z.number().int().nonnegative().default(0),
56
+ pickupCount: z.number().int().nonnegative().default(0),
57
+ notificationCount: z.number().int().nonnegative().default(0),
58
+ firstPickupAt: z.string().datetime().nullable().default(null),
59
+ longestActivityStartedAt: z.string().datetime().nullable().default(null),
60
+ longestActivityEndedAt: z.string().datetime().nullable().default(null),
61
+ metadata: z.record(z.string(), z.unknown()).default({}),
62
+ apps: z.array(screenTimeAppUsageInputSchema).default([]),
63
+ categories: z.array(screenTimeCategoryUsageInputSchema).default([])
64
+ });
65
+ export const screenTimeSettingsInputSchema = z.object({
66
+ trackingEnabled: z.boolean().default(false),
67
+ syncEnabled: z.boolean().default(true),
68
+ authorizationStatus: screenTimeAuthorizationStatusSchema.default("not_determined"),
69
+ captureState: screenTimeCaptureStateSchema.default("disabled"),
70
+ lastCapturedDayKey: z.string().trim().min(1).nullable().default(null),
71
+ lastCaptureStartedAt: z.string().datetime().nullable().default(null),
72
+ lastCaptureEndedAt: z.string().datetime().nullable().default(null),
73
+ metadata: z.record(z.string(), z.unknown()).default({})
74
+ });
75
+ export const screenTimeSettingsPatchSchema = screenTimeSettingsInputSchema.partial();
76
+ export const screenTimeSyncPayloadSchema = z.object({
77
+ settings: screenTimeSettingsInputSchema.default({}),
78
+ daySummaries: z.array(screenTimeDaySummaryInputSchema).default([]),
79
+ hourlySegments: z.array(screenTimeHourlySegmentInputSchema).default([])
80
+ });
81
+ function nowIso() {
82
+ return new Date().toISOString();
83
+ }
84
+ function round(value, digits = 0) {
85
+ const factor = 10 ** digits;
86
+ return Math.round(value * factor) / factor;
87
+ }
88
+ function safeJsonParse(value, fallback) {
89
+ try {
90
+ return JSON.parse(value);
91
+ }
92
+ catch {
93
+ return fallback;
94
+ }
95
+ }
96
+ function average(values) {
97
+ if (values.length === 0) {
98
+ return 0;
99
+ }
100
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
101
+ }
102
+ const SCREEN_TIME_STALE_AFTER_HOURS = 36;
103
+ function hoursBetween(left, right) {
104
+ return Math.max(0, (Date.parse(left) - Date.parse(right)) / 3_600_000);
105
+ }
106
+ function screenTimeCaptureStats(input) {
107
+ const hasCapture = input.capturedDayCount > 0 || input.capturedHourCount > 0;
108
+ const captureAgeHours = input.lastCaptureEndedAt
109
+ ? round(hoursBetween(nowIso(), input.lastCaptureEndedAt), 1)
110
+ : null;
111
+ const captureWindowDays = input.lastCaptureStartedAt && input.lastCaptureEndedAt
112
+ ? Math.max(1, Math.round(hoursBetween(input.lastCaptureEndedAt, input.lastCaptureStartedAt) / 24))
113
+ : hasCapture
114
+ ? 1
115
+ : 0;
116
+ const captureFreshness = input.authorizationStatus === "unavailable"
117
+ ? "unavailable"
118
+ : !hasCapture
119
+ ? "empty"
120
+ : captureAgeHours !== null && captureAgeHours <= SCREEN_TIME_STALE_AFTER_HOURS
121
+ ? "fresh"
122
+ : "stale";
123
+ return {
124
+ captureFreshness,
125
+ captureAgeHours,
126
+ capturedDayCount: input.capturedDayCount,
127
+ capturedHourCount: input.capturedHourCount,
128
+ captureWindowDays
129
+ };
130
+ }
131
+ function overlapSeconds(leftStart, leftEnd, rightStart, rightEnd) {
132
+ const start = Math.max(Date.parse(leftStart), Date.parse(rightStart));
133
+ const end = Math.min(Date.parse(leftEnd), Date.parse(rightEnd));
134
+ return Math.max(0, Math.round((end - start) / 1000));
135
+ }
136
+ function buildUserFilterClause(userIds) {
137
+ const effectiveUserIds = userIds && userIds.length > 0 ? userIds : [getDefaultUser().id];
138
+ return {
139
+ effectiveUserIds,
140
+ placeholders: effectiveUserIds.map(() => "?").join(", ")
141
+ };
142
+ }
143
+ function ensureScreenTimeSettings(userId) {
144
+ const existing = getDatabase()
145
+ .prepare(`SELECT *
146
+ FROM screen_time_settings
147
+ WHERE user_id = ?`)
148
+ .get(userId);
149
+ if (existing) {
150
+ return existing;
151
+ }
152
+ const now = nowIso();
153
+ getDatabase()
154
+ .prepare(`INSERT INTO screen_time_settings (
155
+ user_id, tracking_enabled, sync_enabled, authorization_status, capture_state,
156
+ last_captured_day_key, last_capture_started_at, last_capture_ended_at,
157
+ metadata_json, created_at, updated_at
158
+ ) VALUES (?, 0, 1, 'not_determined', 'disabled', NULL, NULL, NULL, '{}', ?, ?)`)
159
+ .run(userId, now, now);
160
+ return getDatabase()
161
+ .prepare(`SELECT *
162
+ FROM screen_time_settings
163
+ WHERE user_id = ?`)
164
+ .get(userId);
165
+ }
166
+ function mapScreenTimeSettings(row) {
167
+ return {
168
+ userId: row.user_id,
169
+ trackingEnabled: row.tracking_enabled === 1,
170
+ syncEnabled: row.sync_enabled === 1,
171
+ authorizationStatus: row.authorization_status,
172
+ captureState: row.capture_state,
173
+ lastCapturedDayKey: row.last_captured_day_key,
174
+ lastCaptureStartedAt: row.last_capture_started_at,
175
+ lastCaptureEndedAt: row.last_capture_ended_at,
176
+ metadata: safeJsonParse(row.metadata_json, {}),
177
+ createdAt: row.created_at,
178
+ updatedAt: row.updated_at
179
+ };
180
+ }
181
+ function upsertScreenTimeSettings(userId, input) {
182
+ const now = nowIso();
183
+ getDatabase()
184
+ .prepare(`INSERT INTO screen_time_settings (
185
+ user_id, tracking_enabled, sync_enabled, authorization_status, capture_state,
186
+ last_captured_day_key, last_capture_started_at, last_capture_ended_at,
187
+ metadata_json, created_at, updated_at
188
+ )
189
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
190
+ ON CONFLICT(user_id) DO UPDATE SET
191
+ tracking_enabled = excluded.tracking_enabled,
192
+ sync_enabled = excluded.sync_enabled,
193
+ authorization_status = excluded.authorization_status,
194
+ capture_state = excluded.capture_state,
195
+ last_captured_day_key = excluded.last_captured_day_key,
196
+ last_capture_started_at = excluded.last_capture_started_at,
197
+ last_capture_ended_at = excluded.last_capture_ended_at,
198
+ metadata_json = excluded.metadata_json,
199
+ updated_at = excluded.updated_at`)
200
+ .run(userId, input.trackingEnabled ? 1 : 0, input.syncEnabled ? 1 : 0, input.authorizationStatus, input.captureState, input.lastCapturedDayKey, input.lastCaptureStartedAt, input.lastCaptureEndedAt, JSON.stringify(input.metadata ?? {}), now, now);
201
+ return mapScreenTimeSettings(ensureScreenTimeSettings(userId));
202
+ }
203
+ function listHourlySegmentRows(input) {
204
+ const { effectiveUserIds, placeholders } = buildUserFilterClause(input.userIds);
205
+ const conditions = [`user_id IN (${placeholders})`];
206
+ const params = [...effectiveUserIds];
207
+ if (input.dateKey) {
208
+ conditions.push("date_key = ?");
209
+ params.push(input.dateKey);
210
+ }
211
+ if (input.monthKey) {
212
+ conditions.push("date_key LIKE ?");
213
+ params.push(`${input.monthKey}-%`);
214
+ }
215
+ if (input.startedBefore) {
216
+ conditions.push("started_at < ?");
217
+ params.push(input.startedBefore);
218
+ }
219
+ if (input.endedAfter) {
220
+ conditions.push("ended_at > ?");
221
+ params.push(input.endedAfter);
222
+ }
223
+ return getDatabase()
224
+ .prepare(`SELECT *
225
+ FROM screen_time_hourly_segments
226
+ WHERE ${conditions.join(" AND ")}
227
+ ORDER BY started_at ASC`)
228
+ .all(...params);
229
+ }
230
+ function listDaySummaryRows(input) {
231
+ const { effectiveUserIds, placeholders } = buildUserFilterClause(input.userIds);
232
+ const conditions = [`user_id IN (${placeholders})`];
233
+ const params = [...effectiveUserIds];
234
+ if (input.dateKey) {
235
+ conditions.push("date_key = ?");
236
+ params.push(input.dateKey);
237
+ }
238
+ if (input.monthKey) {
239
+ conditions.push("date_key LIKE ?");
240
+ params.push(`${input.monthKey}-%`);
241
+ }
242
+ return getDatabase()
243
+ .prepare(`SELECT *
244
+ FROM screen_time_day_summaries
245
+ WHERE ${conditions.join(" AND ")}
246
+ ORDER BY date_key ASC`)
247
+ .all(...params);
248
+ }
249
+ function listAppUsageRows(segmentIds) {
250
+ if (segmentIds.length === 0) {
251
+ return [];
252
+ }
253
+ const placeholders = segmentIds.map(() => "?").join(", ");
254
+ return getDatabase()
255
+ .prepare(`SELECT *
256
+ FROM screen_time_app_usage
257
+ WHERE segment_id IN (${placeholders})
258
+ ORDER BY total_activity_seconds DESC, display_name ASC`)
259
+ .all(...segmentIds);
260
+ }
261
+ function listCategoryUsageRows(segmentIds) {
262
+ if (segmentIds.length === 0) {
263
+ return [];
264
+ }
265
+ const placeholders = segmentIds.map(() => "?").join(", ");
266
+ return getDatabase()
267
+ .prepare(`SELECT *
268
+ FROM screen_time_category_usage
269
+ WHERE segment_id IN (${placeholders})
270
+ ORDER BY total_activity_seconds DESC, category_label ASC`)
271
+ .all(...segmentIds);
272
+ }
273
+ function mapAppUsage(row) {
274
+ return {
275
+ id: row.id,
276
+ bundleIdentifier: row.bundle_identifier,
277
+ displayName: row.display_name,
278
+ categoryLabel: row.category_label,
279
+ totalActivitySeconds: row.total_activity_seconds,
280
+ pickupCount: row.pickup_count,
281
+ notificationCount: row.notification_count
282
+ };
283
+ }
284
+ function mapCategoryUsage(row) {
285
+ return {
286
+ id: row.id,
287
+ categoryLabel: row.category_label,
288
+ totalActivitySeconds: row.total_activity_seconds
289
+ };
290
+ }
291
+ function mapHourlySegment(row, apps, categories) {
292
+ return {
293
+ id: row.id,
294
+ userId: row.user_id,
295
+ pairingSessionId: row.pairing_session_id,
296
+ sourceDevice: row.source_device,
297
+ dateKey: row.date_key,
298
+ hourIndex: row.hour_index,
299
+ startedAt: row.started_at,
300
+ endedAt: row.ended_at,
301
+ totalActivitySeconds: row.total_activity_seconds,
302
+ pickupCount: row.pickup_count,
303
+ notificationCount: row.notification_count,
304
+ firstPickupAt: row.first_pickup_at,
305
+ longestActivityStartedAt: row.longest_activity_started_at,
306
+ longestActivityEndedAt: row.longest_activity_ended_at,
307
+ metadata: safeJsonParse(row.metadata_json, {}),
308
+ apps: apps.map(mapAppUsage),
309
+ categories: categories.map(mapCategoryUsage)
310
+ };
311
+ }
312
+ function aggregateAppUsage(rows) {
313
+ const byBundle = new Map();
314
+ rows.forEach((row) => {
315
+ const existing = byBundle.get(row.bundleIdentifier);
316
+ if (existing) {
317
+ existing.totalActivitySeconds += row.totalActivitySeconds;
318
+ existing.pickupCount += row.pickupCount;
319
+ existing.notificationCount += row.notificationCount;
320
+ if (!existing.displayName && row.displayName) {
321
+ existing.displayName = row.displayName;
322
+ }
323
+ if (!existing.categoryLabel && row.categoryLabel) {
324
+ existing.categoryLabel = row.categoryLabel;
325
+ }
326
+ return;
327
+ }
328
+ byBundle.set(row.bundleIdentifier, {
329
+ id: `app_${row.bundleIdentifier}`,
330
+ bundleIdentifier: row.bundleIdentifier,
331
+ displayName: row.displayName,
332
+ categoryLabel: row.categoryLabel,
333
+ totalActivitySeconds: row.totalActivitySeconds,
334
+ pickupCount: row.pickupCount,
335
+ notificationCount: row.notificationCount
336
+ });
337
+ });
338
+ return [...byBundle.values()].sort((left, right) => right.totalActivitySeconds - left.totalActivitySeconds ||
339
+ right.pickupCount - left.pickupCount ||
340
+ left.displayName.localeCompare(right.displayName));
341
+ }
342
+ function aggregateCategoryUsage(rows) {
343
+ const byCategory = new Map();
344
+ rows.forEach((row) => {
345
+ const existing = byCategory.get(row.categoryLabel);
346
+ if (existing) {
347
+ existing.totalActivitySeconds += row.totalActivitySeconds;
348
+ return;
349
+ }
350
+ byCategory.set(row.categoryLabel, {
351
+ id: `cat_${row.categoryLabel}`,
352
+ categoryLabel: row.categoryLabel,
353
+ totalActivitySeconds: row.totalActivitySeconds
354
+ });
355
+ });
356
+ return [...byCategory.values()].sort((left, right) => right.totalActivitySeconds - left.totalActivitySeconds ||
357
+ left.categoryLabel.localeCompare(right.categoryLabel));
358
+ }
359
+ function upsertDaySummary(pairing, sourceDevice, summary) {
360
+ const existing = getDatabase()
361
+ .prepare(`SELECT id
362
+ FROM screen_time_day_summaries
363
+ WHERE user_id = ?
364
+ AND source_device = ?
365
+ AND date_key = ?`)
366
+ .get(pairing.user_id, sourceDevice, summary.dateKey);
367
+ const id = existing?.id ?? `std_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
368
+ const now = nowIso();
369
+ getDatabase()
370
+ .prepare(`INSERT INTO screen_time_day_summaries (
371
+ id, user_id, pairing_session_id, source_device, date_key,
372
+ total_activity_seconds, pickup_count, notification_count, first_pickup_at,
373
+ longest_activity_seconds, top_app_bundle_ids_json, top_category_labels_json,
374
+ metadata_json, created_at, updated_at
375
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
376
+ ON CONFLICT(user_id, source_device, date_key) DO UPDATE SET
377
+ pairing_session_id = excluded.pairing_session_id,
378
+ total_activity_seconds = excluded.total_activity_seconds,
379
+ pickup_count = excluded.pickup_count,
380
+ notification_count = excluded.notification_count,
381
+ first_pickup_at = excluded.first_pickup_at,
382
+ longest_activity_seconds = excluded.longest_activity_seconds,
383
+ top_app_bundle_ids_json = excluded.top_app_bundle_ids_json,
384
+ top_category_labels_json = excluded.top_category_labels_json,
385
+ metadata_json = excluded.metadata_json,
386
+ updated_at = excluded.updated_at`)
387
+ .run(id, pairing.user_id, pairing.id, sourceDevice, summary.dateKey, summary.totalActivitySeconds, summary.pickupCount, summary.notificationCount, summary.firstPickupAt, summary.longestActivitySeconds, JSON.stringify(summary.topAppBundleIdentifiers), JSON.stringify(summary.topCategoryLabels), JSON.stringify(summary.metadata ?? {}), now, now);
388
+ return existing ? "updated" : "created";
389
+ }
390
+ function replaceSegmentChildren(segmentId, apps, categories) {
391
+ getDatabase()
392
+ .prepare(`DELETE FROM screen_time_app_usage WHERE segment_id = ?`)
393
+ .run(segmentId);
394
+ getDatabase()
395
+ .prepare(`DELETE FROM screen_time_category_usage WHERE segment_id = ?`)
396
+ .run(segmentId);
397
+ const now = nowIso();
398
+ const insertApp = getDatabase().prepare(`INSERT INTO screen_time_app_usage (
399
+ id, segment_id, bundle_identifier, display_name, category_label,
400
+ total_activity_seconds, pickup_count, notification_count, created_at, updated_at
401
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
402
+ apps.forEach((app) => {
403
+ insertApp.run(`sta_${randomUUID().replaceAll("-", "").slice(0, 10)}`, segmentId, app.bundleIdentifier, app.displayName, app.categoryLabel, app.totalActivitySeconds, app.pickupCount, app.notificationCount, now, now);
404
+ });
405
+ const insertCategory = getDatabase().prepare(`INSERT INTO screen_time_category_usage (
406
+ id, segment_id, category_label, total_activity_seconds, created_at, updated_at
407
+ ) VALUES (?, ?, ?, ?, ?, ?)`);
408
+ categories.forEach((category) => {
409
+ insertCategory.run(`stc_${randomUUID().replaceAll("-", "").slice(0, 10)}`, segmentId, category.categoryLabel, category.totalActivitySeconds, now, now);
410
+ });
411
+ }
412
+ function upsertHourlySegment(pairing, sourceDevice, segment) {
413
+ const existing = getDatabase()
414
+ .prepare(`SELECT id
415
+ FROM screen_time_hourly_segments
416
+ WHERE user_id = ?
417
+ AND source_device = ?
418
+ AND date_key = ?
419
+ AND hour_index = ?`)
420
+ .get(pairing.user_id, sourceDevice, segment.dateKey, segment.hourIndex);
421
+ const id = existing?.id ?? `sth_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
422
+ const now = nowIso();
423
+ getDatabase()
424
+ .prepare(`INSERT INTO screen_time_hourly_segments (
425
+ id, user_id, pairing_session_id, source_device, date_key, hour_index,
426
+ started_at, ended_at, total_activity_seconds, pickup_count, notification_count,
427
+ first_pickup_at, longest_activity_started_at, longest_activity_ended_at,
428
+ metadata_json, created_at, updated_at
429
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
430
+ ON CONFLICT(user_id, source_device, date_key, hour_index) DO UPDATE SET
431
+ pairing_session_id = excluded.pairing_session_id,
432
+ started_at = excluded.started_at,
433
+ ended_at = excluded.ended_at,
434
+ total_activity_seconds = excluded.total_activity_seconds,
435
+ pickup_count = excluded.pickup_count,
436
+ notification_count = excluded.notification_count,
437
+ first_pickup_at = excluded.first_pickup_at,
438
+ longest_activity_started_at = excluded.longest_activity_started_at,
439
+ longest_activity_ended_at = excluded.longest_activity_ended_at,
440
+ metadata_json = excluded.metadata_json,
441
+ updated_at = excluded.updated_at`)
442
+ .run(id, pairing.user_id, pairing.id, sourceDevice, segment.dateKey, segment.hourIndex, segment.startedAt, segment.endedAt, segment.totalActivitySeconds, segment.pickupCount, segment.notificationCount, segment.firstPickupAt, segment.longestActivityStartedAt, segment.longestActivityEndedAt, JSON.stringify(segment.metadata ?? {}), now, now);
443
+ replaceSegmentChildren(id, segment.apps, segment.categories);
444
+ return existing ? "updated" : "created";
445
+ }
446
+ export function ingestScreenTimeSync(pairing, payload, sourceDevice = "iPhone") {
447
+ const parsed = screenTimeSyncPayloadSchema.parse(payload);
448
+ const settings = upsertScreenTimeSettings(pairing.user_id, parsed.settings);
449
+ let createdCount = 0;
450
+ let updatedCount = 0;
451
+ parsed.daySummaries.forEach((summary) => {
452
+ const mode = upsertDaySummary(pairing, sourceDevice, summary);
453
+ if (mode === "created") {
454
+ createdCount += 1;
455
+ }
456
+ else {
457
+ updatedCount += 1;
458
+ }
459
+ });
460
+ parsed.hourlySegments.forEach((segment) => {
461
+ const mode = upsertHourlySegment(pairing, sourceDevice, segment);
462
+ if (mode === "created") {
463
+ createdCount += 1;
464
+ }
465
+ else {
466
+ updatedCount += 1;
467
+ }
468
+ });
469
+ recordDiagnosticLog({
470
+ level: parsed.settings.authorizationStatus === "denied" ||
471
+ parsed.settings.authorizationStatus === "unavailable"
472
+ ? "warning"
473
+ : "info",
474
+ scope: "screen_time_sync",
475
+ eventKey: "screen_time_sync_ingested",
476
+ message: parsed.settings.authorizationStatus === "approved"
477
+ ? `Ingested Screen Time payload with ${parsed.hourlySegments.length} hourly segments.`
478
+ : `Screen Time sync reported ${parsed.settings.authorizationStatus}.`,
479
+ entityType: "system",
480
+ entityId: pairing.id,
481
+ details: {
482
+ userId: pairing.user_id,
483
+ authorizationStatus: parsed.settings.authorizationStatus,
484
+ captureState: parsed.settings.captureState,
485
+ sourceDevice,
486
+ daySummaries: parsed.daySummaries.length,
487
+ hourlySegments: parsed.hourlySegments.length,
488
+ createdCount,
489
+ updatedCount
490
+ }
491
+ });
492
+ if (parsed.settings.authorizationStatus === "approved" &&
493
+ parsed.hourlySegments.length === 0) {
494
+ recordDiagnosticLog({
495
+ level: "warning",
496
+ scope: "screen_time_sync",
497
+ eventKey: "screen_time_capture_empty",
498
+ message: "Screen Time was approved but no hourly segments were synced.",
499
+ entityType: "system",
500
+ entityId: pairing.id,
501
+ details: {
502
+ userId: pairing.user_id,
503
+ sourceDevice,
504
+ lastCapturedDayKey: parsed.settings.lastCapturedDayKey,
505
+ captureState: parsed.settings.captureState
506
+ }
507
+ });
508
+ }
509
+ return {
510
+ settings,
511
+ createdCount,
512
+ updatedCount,
513
+ daySummaries: parsed.daySummaries.length,
514
+ hourlySegments: parsed.hourlySegments.length
515
+ };
516
+ }
517
+ export function getScreenTimeSettings(userIds) {
518
+ const effectiveUserId = userIds?.[0] ?? getDefaultUser().id;
519
+ const settings = mapScreenTimeSettings(ensureScreenTimeSettings(effectiveUserId));
520
+ const capturedDayCount = getDatabase()
521
+ .prepare(`SELECT COUNT(*) as count
522
+ FROM screen_time_day_summaries
523
+ WHERE user_id = ?`)
524
+ .get(effectiveUserId);
525
+ const capturedHourCount = getDatabase()
526
+ .prepare(`SELECT COUNT(*) as count
527
+ FROM screen_time_hourly_segments
528
+ WHERE user_id = ?`)
529
+ .get(effectiveUserId);
530
+ return {
531
+ ...settings,
532
+ ...screenTimeCaptureStats({
533
+ authorizationStatus: settings.authorizationStatus,
534
+ lastCaptureStartedAt: settings.lastCaptureStartedAt,
535
+ lastCaptureEndedAt: settings.lastCaptureEndedAt,
536
+ capturedDayCount: capturedDayCount.count,
537
+ capturedHourCount: capturedHourCount.count
538
+ })
539
+ };
540
+ }
541
+ export function updateScreenTimeSettings(userId, patch) {
542
+ const existing = getScreenTimeSettings([userId]);
543
+ const parsed = screenTimeSettingsPatchSchema.parse(patch);
544
+ return upsertScreenTimeSettings(userId, {
545
+ trackingEnabled: parsed.trackingEnabled ?? existing.trackingEnabled,
546
+ syncEnabled: parsed.syncEnabled ?? existing.syncEnabled,
547
+ authorizationStatus: parsed.authorizationStatus ?? existing.authorizationStatus,
548
+ captureState: parsed.captureState ?? existing.captureState,
549
+ lastCapturedDayKey: parsed.lastCapturedDayKey ?? existing.lastCapturedDayKey,
550
+ lastCaptureStartedAt: parsed.lastCaptureStartedAt ?? existing.lastCaptureStartedAt,
551
+ lastCaptureEndedAt: parsed.lastCaptureEndedAt ?? existing.lastCaptureEndedAt,
552
+ metadata: {
553
+ ...existing.metadata,
554
+ ...(parsed.metadata ?? {})
555
+ }
556
+ });
557
+ }
558
+ export function getScreenTimeOverlapSummary(input) {
559
+ const segmentRows = listHourlySegmentRows({
560
+ userIds: input.userIds,
561
+ startedBefore: input.endedAt,
562
+ endedAfter: input.startedAt
563
+ });
564
+ if (segmentRows.length === 0) {
565
+ return {
566
+ estimatedScreenTimeSeconds: 0,
567
+ pickupCount: 0,
568
+ notificationCount: 0,
569
+ topApps: [],
570
+ topCategories: []
571
+ };
572
+ }
573
+ const appRowsBySegment = new Map();
574
+ listAppUsageRows(segmentRows.map((row) => row.id)).forEach((row) => {
575
+ appRowsBySegment.set(row.segment_id, [...(appRowsBySegment.get(row.segment_id) ?? []), row]);
576
+ });
577
+ const categoryRowsBySegment = new Map();
578
+ listCategoryUsageRows(segmentRows.map((row) => row.id)).forEach((row) => {
579
+ categoryRowsBySegment.set(row.segment_id, [
580
+ ...(categoryRowsBySegment.get(row.segment_id) ?? []),
581
+ row
582
+ ]);
583
+ });
584
+ let estimatedScreenTimeSeconds = 0;
585
+ let pickupCount = 0;
586
+ let notificationCount = 0;
587
+ const weightedApps = [];
588
+ const weightedCategories = [];
589
+ segmentRows.forEach((row) => {
590
+ const overlap = overlapSeconds(input.startedAt, input.endedAt, row.started_at, row.ended_at);
591
+ const segmentSeconds = Math.max(1, overlapSeconds(row.started_at, row.ended_at, row.started_at, row.ended_at));
592
+ const ratio = overlap / segmentSeconds;
593
+ if (ratio <= 0) {
594
+ return;
595
+ }
596
+ estimatedScreenTimeSeconds += row.total_activity_seconds * ratio;
597
+ pickupCount += row.pickup_count * ratio;
598
+ notificationCount += row.notification_count * ratio;
599
+ (appRowsBySegment.get(row.id) ?? []).forEach((app) => {
600
+ weightedApps.push({
601
+ bundleIdentifier: app.bundle_identifier,
602
+ displayName: app.display_name,
603
+ categoryLabel: app.category_label,
604
+ totalActivitySeconds: app.total_activity_seconds * ratio,
605
+ pickupCount: app.pickup_count * ratio,
606
+ notificationCount: app.notification_count * ratio
607
+ });
608
+ });
609
+ (categoryRowsBySegment.get(row.id) ?? []).forEach((category) => {
610
+ weightedCategories.push({
611
+ categoryLabel: category.category_label,
612
+ totalActivitySeconds: category.total_activity_seconds * ratio
613
+ });
614
+ });
615
+ });
616
+ return {
617
+ estimatedScreenTimeSeconds: Math.round(estimatedScreenTimeSeconds),
618
+ pickupCount: Math.round(pickupCount),
619
+ notificationCount: Math.round(notificationCount),
620
+ topApps: aggregateAppUsage(weightedApps).slice(0, 4).map((app) => ({
621
+ ...app,
622
+ totalActivitySeconds: Math.round(app.totalActivitySeconds),
623
+ pickupCount: Math.round(app.pickupCount),
624
+ notificationCount: Math.round(app.notificationCount)
625
+ })),
626
+ topCategories: aggregateCategoryUsage(weightedCategories)
627
+ .slice(0, 4)
628
+ .map((category) => ({
629
+ ...category,
630
+ totalActivitySeconds: Math.round(category.totalActivitySeconds)
631
+ }))
632
+ };
633
+ }
634
+ export function getScreenTimeDayDetail(input) {
635
+ const targetDate = input.date ?? new Date().toISOString().slice(0, 10);
636
+ const segmentRows = listHourlySegmentRows({
637
+ userIds: input.userIds,
638
+ dateKey: targetDate
639
+ });
640
+ const appRows = listAppUsageRows(segmentRows.map((row) => row.id));
641
+ const categoryRows = listCategoryUsageRows(segmentRows.map((row) => row.id));
642
+ const appRowsBySegment = new Map();
643
+ appRows.forEach((row) => {
644
+ appRowsBySegment.set(row.segment_id, [...(appRowsBySegment.get(row.segment_id) ?? []), row]);
645
+ });
646
+ const categoryRowsBySegment = new Map();
647
+ categoryRows.forEach((row) => {
648
+ categoryRowsBySegment.set(row.segment_id, [
649
+ ...(categoryRowsBySegment.get(row.segment_id) ?? []),
650
+ row
651
+ ]);
652
+ });
653
+ const hourlySegments = segmentRows.map((row) => mapHourlySegment(row, appRowsBySegment.get(row.id) ?? [], categoryRowsBySegment.get(row.id) ?? []));
654
+ const dayRows = listDaySummaryRows({
655
+ userIds: input.userIds,
656
+ dateKey: targetDate
657
+ });
658
+ const fallbackSummary = {
659
+ totalActivitySeconds: hourlySegments.reduce((sum, segment) => sum + segment.totalActivitySeconds, 0),
660
+ pickupCount: hourlySegments.reduce((sum, segment) => sum + segment.pickupCount, 0),
661
+ notificationCount: hourlySegments.reduce((sum, segment) => sum + segment.notificationCount, 0),
662
+ longestActivitySeconds: hourlySegments.reduce((max, segment) => {
663
+ if (!segment.longestActivityStartedAt ||
664
+ !segment.longestActivityEndedAt) {
665
+ return max;
666
+ }
667
+ const seconds = overlapSeconds(segment.longestActivityStartedAt, segment.longestActivityEndedAt, segment.longestActivityStartedAt, segment.longestActivityEndedAt);
668
+ return Math.max(max, seconds);
669
+ }, 0)
670
+ };
671
+ const topApps = aggregateAppUsage(appRows.map((row) => ({
672
+ bundleIdentifier: row.bundle_identifier,
673
+ displayName: row.display_name,
674
+ categoryLabel: row.category_label,
675
+ totalActivitySeconds: row.total_activity_seconds,
676
+ pickupCount: row.pickup_count,
677
+ notificationCount: row.notification_count
678
+ })));
679
+ const topCategories = aggregateCategoryUsage(categoryRows.map((row) => ({
680
+ categoryLabel: row.category_label,
681
+ totalActivitySeconds: row.total_activity_seconds
682
+ })));
683
+ const firstPickupCandidates = dayRows
684
+ .map((row) => row.first_pickup_at)
685
+ .filter((value) => Boolean(value))
686
+ .sort((left, right) => Date.parse(left) - Date.parse(right));
687
+ return {
688
+ date: targetDate,
689
+ settings: getScreenTimeSettings(input.userIds),
690
+ summary: {
691
+ totalActivitySeconds: dayRows.reduce((sum, row) => sum + row.total_activity_seconds, 0) ||
692
+ fallbackSummary.totalActivitySeconds,
693
+ pickupCount: dayRows.reduce((sum, row) => sum + row.pickup_count, 0) ||
694
+ fallbackSummary.pickupCount,
695
+ notificationCount: dayRows.reduce((sum, row) => sum + row.notification_count, 0) ||
696
+ fallbackSummary.notificationCount,
697
+ firstPickupAt: firstPickupCandidates[0] ?? null,
698
+ longestActivitySeconds: dayRows.reduce((max, row) => Math.max(max, row.longest_activity_seconds), 0) ||
699
+ fallbackSummary.longestActivitySeconds,
700
+ activeHourCount: hourlySegments.filter((segment) => segment.totalActivitySeconds > 0).length,
701
+ averageHourlyActivitySeconds: round(average(hourlySegments
702
+ .map((segment) => segment.totalActivitySeconds)
703
+ .filter((value) => value > 0)))
704
+ },
705
+ hourlySegments,
706
+ topApps: topApps.slice(0, 8),
707
+ topCategories: topCategories.slice(0, 8)
708
+ };
709
+ }
710
+ export function getScreenTimeMonthSummary(input) {
711
+ const monthKey = input.month ?? new Date().toISOString().slice(0, 7);
712
+ const dayRows = listDaySummaryRows({
713
+ userIds: input.userIds,
714
+ monthKey
715
+ });
716
+ const segmentRows = listHourlySegmentRows({
717
+ userIds: input.userIds,
718
+ monthKey
719
+ });
720
+ const appRows = listAppUsageRows(segmentRows.map((row) => row.id));
721
+ const categoryRows = listCategoryUsageRows(segmentRows.map((row) => row.id));
722
+ return {
723
+ month: monthKey,
724
+ days: dayRows.map((row) => ({
725
+ dateKey: row.date_key,
726
+ totalActivitySeconds: row.total_activity_seconds,
727
+ pickupCount: row.pickup_count,
728
+ notificationCount: row.notification_count,
729
+ longestActivitySeconds: row.longest_activity_seconds
730
+ })),
731
+ totals: {
732
+ totalActivitySeconds: dayRows.reduce((sum, row) => sum + row.total_activity_seconds, 0),
733
+ pickupCount: dayRows.reduce((sum, row) => sum + row.pickup_count, 0),
734
+ notificationCount: dayRows.reduce((sum, row) => sum + row.notification_count, 0),
735
+ activeDays: dayRows.filter((row) => row.total_activity_seconds > 0).length
736
+ },
737
+ topApps: aggregateAppUsage(appRows.map((row) => ({
738
+ bundleIdentifier: row.bundle_identifier,
739
+ displayName: row.display_name,
740
+ categoryLabel: row.category_label,
741
+ totalActivitySeconds: row.total_activity_seconds,
742
+ pickupCount: row.pickup_count,
743
+ notificationCount: row.notification_count
744
+ }))).slice(0, 10),
745
+ topCategories: aggregateCategoryUsage(categoryRows.map((row) => ({
746
+ categoryLabel: row.category_label,
747
+ totalActivitySeconds: row.total_activity_seconds
748
+ }))).slice(0, 10)
749
+ };
750
+ }
751
+ export function getScreenTimeAllTimeSummary(userIds) {
752
+ const dayRows = listDaySummaryRows({ userIds });
753
+ const segmentRows = listHourlySegmentRows({ userIds });
754
+ const appRows = listAppUsageRows(segmentRows.map((row) => row.id));
755
+ const categoryRows = listCategoryUsageRows(segmentRows.map((row) => row.id));
756
+ const weekdayMap = new Map();
757
+ dayRows.forEach((row) => {
758
+ const weekday = new Date(`${row.date_key}T12:00:00.000Z`).getUTCDay();
759
+ const existing = weekdayMap.get(weekday) ?? {
760
+ weekday,
761
+ totalActivitySeconds: 0,
762
+ pickupCount: 0,
763
+ notificationCount: 0,
764
+ days: 0
765
+ };
766
+ existing.totalActivitySeconds += row.total_activity_seconds;
767
+ existing.pickupCount += row.pickup_count;
768
+ existing.notificationCount += row.notification_count;
769
+ existing.days += 1;
770
+ weekdayMap.set(weekday, existing);
771
+ });
772
+ return {
773
+ summary: {
774
+ dayCount: dayRows.length,
775
+ totalActivitySeconds: dayRows.reduce((sum, row) => sum + row.total_activity_seconds, 0),
776
+ totalPickups: dayRows.reduce((sum, row) => sum + row.pickup_count, 0),
777
+ totalNotifications: dayRows.reduce((sum, row) => sum + row.notification_count, 0),
778
+ averageDailyActivitySeconds: round(average(dayRows.map((row) => row.total_activity_seconds))),
779
+ averageDailyPickups: round(average(dayRows.map((row) => row.pickup_count)), 1)
780
+ },
781
+ weekdayPattern: [...weekdayMap.values()]
782
+ .sort((left, right) => left.weekday - right.weekday)
783
+ .map((row) => ({
784
+ weekday: row.weekday,
785
+ averageActivitySeconds: round(row.totalActivitySeconds / Math.max(1, row.days)),
786
+ averagePickups: round(row.pickupCount / Math.max(1, row.days), 1),
787
+ averageNotifications: round(row.notificationCount / Math.max(1, row.days), 1)
788
+ })),
789
+ topApps: aggregateAppUsage(appRows.map((row) => ({
790
+ bundleIdentifier: row.bundle_identifier,
791
+ displayName: row.display_name,
792
+ categoryLabel: row.category_label,
793
+ totalActivitySeconds: row.total_activity_seconds,
794
+ pickupCount: row.pickup_count,
795
+ notificationCount: row.notification_count
796
+ }))).slice(0, 12),
797
+ topCategories: aggregateCategoryUsage(categoryRows.map((row) => ({
798
+ categoryLabel: row.category_label,
799
+ totalActivitySeconds: row.total_activity_seconds
800
+ }))).slice(0, 12)
801
+ };
802
+ }