main.vala 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. using Astralis;
  2. using Inversion;
  3. using InvercargillSqlInversion;
  4. using Spry;
  5. using Spry.Authentication;
  6. using Statum;
  7. using ClankerSaddle;
  8. using ClankerSaddle.Api;
  9. using ClankerSaddle.Data;
  10. using ClankerSaddle.Services;
  11. int main(string[] args) {
  12. int port = args.length > 1 ? int.parse(args[1]) : 8080;
  13. print("ClankerSaddle - LLM task orchestration platform\n");
  14. print(@"Website starting on port $(port)\n");
  15. try {
  16. var application = new WebApplication(port);
  17. application.use_compression();
  18. application.add_module<StatumModule>();
  19. application.add_module<SpryModule>();
  20. var db_path = Environment.get_variable("SPRY_DB_PATH") ?? "";
  21. if (db_path.strip().length == 0) {
  22. db_path = "./clanker-saddle.sqlite";
  23. }
  24. var db = application.container.configure_with<DatabaseConfigurator>();
  25. db.register_connection(@"sqlite://$(db_path)");
  26. // SQLite serialisation posture (the flake family where a SELECT
  27. // stepping against a concurrent write lock — the test harness's
  28. // fixture writes share this file — returned SQLITE_BUSY, which
  29. // the ORM's lazy enumerator reports as end-of-rows: an EMPTY
  30. // result for data that exists): WAL readers never block on (and
  31. // are never blocked by) writers, and the busy timeout absorbs
  32. // the remaining writer-writer window.
  33. try {
  34. var connection = application.container.create_scope()
  35. .resolve<InvercargillSql.Connection>();
  36. connection.execute("PRAGMA journal_mode = WAL");
  37. connection.execute("PRAGMA busy_timeout = 5000");
  38. } catch (Error pragma_error) {
  39. print(@"[ClankerSaddle] sqlite pragmas failed: $(pragma_error.message)\n");
  40. }
  41. db.add_migration<ClankerSaddle.Data.Migrations.M0001_Initial>();
  42. db.add_migration<ClankerSaddle.Data.Migrations.M0002_ForceWake>();
  43. db.add_entity<Project>(Project.entity_mapping);
  44. db.add_entity<ProjectLink>(ProjectLink.entity_mapping);
  45. db.add_entity<TaskEntity>(TaskEntity.entity_mapping);
  46. db.add_entity<TaskComment>(TaskComment.entity_mapping);
  47. db.add_entity<TaskBlock>(TaskBlock.entity_mapping);
  48. db.add_entity<TaskProgress>(TaskProgress.entity_mapping);
  49. db.add_entity<Saddle>(Saddle.entity_mapping);
  50. db.add_entity<Session>(Session.entity_mapping);
  51. db.add_entity<SessionCommand>(SessionCommand.entity_mapping);
  52. db.add_entity<SaddleEntryDedup>(SaddleEntryDedup.entity_mapping);
  53. db.add_entity<Rule>(Rule.entity_mapping);
  54. db.add_entity<RuleProject>(RuleProject.entity_mapping);
  55. db.add_entity<TaskRule>(TaskRule.entity_mapping);
  56. db.add_entity<ProjectNote>(ProjectNote.entity_mapping);
  57. db.add_entity<SessionRawLog>(SessionRawLog.entity_mapping);
  58. db.add_entity<Capability>(Capability.entity_mapping);
  59. db.add_entity<SaddleCapability>(SaddleCapability.entity_mapping);
  60. db.add_entity<TaskCapability>(TaskCapability.entity_mapping);
  61. db.add_entity<PushSubscription>(PushSubscription.entity_mapping);
  62. db.add_entity<TaskNotifyPref>(TaskNotifyPref.entity_mapping);
  63. db.add_entity<Goal>(Goal.entity_mapping);
  64. db.add_entity<GoalLink>(GoalLink.entity_mapping);
  65. db.migrate_on_startup();
  66. application.add_scoped<ProjectService>();
  67. application.add_scoped<TaskService>();
  68. application.add_scoped<GoalService>();
  69. application.add_scoped<SaddleService>();
  70. application.add_scoped<LifecycleService>();
  71. application.add_scoped<RuleService>();
  72. application.add_scoped<CapabilityService>();
  73. application.add_scoped<AllocationService>();
  74. application.add_scoped<TimelineService>();
  75. application.add_singleton<SaddlePollState>();
  76. application.add_singleton<StatePulse>();
  77. application.add_singleton<PushService>();
  78. application.add_singleton<Dispatcher>();
  79. // Web Push (plan 11 task E): the VAPID keypair loads or
  80. // generates + persists BEFORE the server accepts requests, so
  81. // every subscriber sees the same public key from the first
  82. // page render.
  83. application.container.create_scope()
  84. .resolve<PushService>().ensure_keys_from_config();
  85. var statum = application.configure_with<StatumConfigurator>();
  86. statum.global<ClankerSaddle.Services.PulsePublic>(StatePulse.SLOT_TYPE,
  87. new ClankerSaddle.Services.PulsePublic());
  88. // spry:pages-begin
  89. statum.add_page<HomePage, HomeEntrypoint>();
  90. statum.add_page<LoginPage, LoginEntrypoint>();
  91. statum.add_page<ProjectsPage, ProjectsEntrypoint>();
  92. statum.add_page<ProjectDetailPage, ProjectDetailEntrypoint>();
  93. statum.add_page<TasksPage, TasksEntrypoint>();
  94. statum.add_page<TaskDetailPage, TaskDetailEntrypoint>();
  95. statum.add_page<TaskTerminalPage, TaskTerminalEntrypoint>();
  96. statum.add_page<GoalsPage, GoalsEntrypoint>();
  97. statum.add_page<GoalDetailPage, GoalDetailEntrypoint>();
  98. statum.add_page<SaddlesPage, SaddlesEntrypoint>();
  99. statum.add_page<RulesPage, RulesEntrypoint>();
  100. statum.add_page<UsersPage, UsersEntrypoint>();
  101. statum.add_page<ProfilePage, ProfileEntrypoint>();
  102. // spry:pages-end
  103. // spry:actions-begin
  104. statum.action<LoginAction>();
  105. statum.action<Spry.Actions.LogoutAction>();
  106. statum.action<CreateProjectAction>();
  107. statum.action<UpdateProjectAction>();
  108. statum.action<AddProjectLinkAction>();
  109. statum.action<RemoveProjectLinkAction>();
  110. statum.action<CreateTaskAction>();
  111. statum.action<UpdateTaskAction>();
  112. statum.action<AddCommentAction>();
  113. statum.action<AnswerBlockAction>();
  114. statum.action<EnqueueTaskAction>();
  115. statum.action<DeleteTaskAction>();
  116. statum.action<CancelTaskAction>();
  117. statum.action<RetryTaskAction>();
  118. statum.action<ResumeTaskAction>();
  119. statum.action<TaskForceWakeAction>();
  120. statum.action<RefreshTasksDataAction>();
  121. statum.action<CreateGoalAction>();
  122. statum.action<StartGoalAction>();
  123. statum.action<UpdateDraftGoalAction>();
  124. statum.action<PauseGoalAction>();
  125. statum.action<ResumeGoalAction>();
  126. statum.action<GoalForceWakeAction>();
  127. statum.action<CancelGoalAction>();
  128. statum.action<RetryGoalRunAction>();
  129. statum.action<RequeueGoalRunAction>();
  130. statum.action<GoalCommentAction>();
  131. statum.action<AnswerGoalBlockAction>();
  132. statum.action<AddGoalLinkAction>();
  133. statum.action<RemoveGoalLinkAction>();
  134. statum.action<RefreshGoalDataAction>();
  135. statum.action<PushSubscribeAction>();
  136. statum.action<PushUnsubscribeAction>();
  137. statum.action<PushTestAction>();
  138. statum.action<ToggleTaskNotifyAction>();
  139. statum.action<RefreshTaskDataAction>();
  140. statum.action<RefreshDashboardAction>();
  141. statum.action<RegisterSaddleAction>();
  142. statum.action<UnregisterSaddleAction>();
  143. statum.action<RefreshSaddleStatusAction>();
  144. statum.action<UpdateSaddleIdentityAction>();
  145. statum.action<CreateRuleAction>();
  146. statum.action<UpdateRuleAction>();
  147. statum.action<DeleteRuleAction>();
  148. statum.action<AttachProjectRuleAction>();
  149. statum.action<DetachProjectRuleAction>();
  150. statum.action<AttachTaskRuleAction>();
  151. statum.action<DetachTaskRuleAction>();
  152. statum.action<DeleteProjectNoteAction>();
  153. statum.action<CreateCapabilityAction>();
  154. statum.action<DeleteCapabilityAction>();
  155. statum.action<AttachSaddleCapabilityAction>();
  156. statum.action<DetachSaddleCapabilityAction>();
  157. statum.action<AttachTaskCapabilityAction>();
  158. statum.action<DetachTaskCapabilityAction>();
  159. statum.action<CreateUserAction>();
  160. statum.action<AlterManagedUserAction>();
  161. statum.action<SetManagedUserEnabledAction>();
  162. statum.action<GrantUserAdminAction>();
  163. statum.action<RevokeUserAdminAction>();
  164. statum.action<DeleteManagedUserAction>();
  165. statum.action<ChangeMyPasswordAction>();
  166. // spry:actions-end
  167. // spry:resources-begin
  168. statum.add_resource<ClankerSaddle.AppCss>();
  169. statum.add_resource<ClankerSaddle.FaviconResource>();
  170. statum.add_resource<ClankerSaddle.EmblemResource>();
  171. // spry:resources-end
  172. // spry:auth-begin
  173. // spry:auth-end
  174. application.add_endpoint<SaddleNextTaskEndpoint>(
  175. new EndpointRoute("/api/saddle/next-task", Method.POST));
  176. application.add_endpoint<SaddleChunksEndpoint>(
  177. new EndpointRoute("/api/saddle/chunks", Method.POST));
  178. application.add_endpoint<SaddleResultsEndpoint>(
  179. new EndpointRoute("/api/saddle/results", Method.POST));
  180. application.add_endpoint<SaddleNoteEndpoint>(
  181. new EndpointRoute("/api/saddle/note", Method.POST));
  182. application.add_endpoint<SaddleTaskCreateEndpoint>(
  183. new EndpointRoute("/api/saddle/task/create", Method.POST));
  184. application.add_endpoint<SaddleTaskDeleteEndpoint>(
  185. new EndpointRoute("/api/saddle/task/delete", Method.POST));
  186. application.add_endpoint<SaddleTaskEnqueueEndpoint>(
  187. new EndpointRoute("/api/saddle/task/enqueue", Method.POST));
  188. application.add_endpoint<SaddleTaskListEndpoint>(
  189. new EndpointRoute("/api/saddle/tasks", Method.GET));
  190. application.add_endpoint<SaddleTaskDetailEndpoint>(
  191. new EndpointRoute("/api/saddle/task/{id}", Method.GET));
  192. application.add_endpoint<SaddleCapabilitiesEndpoint>(
  193. new EndpointRoute("/api/saddle/capabilities", Method.GET));
  194. application.add_endpoint<ClankerSaddle.Api.TerminalSseEndpoint>(
  195. new EndpointRoute("/terminal-stream/{session_id}", Method.GET));
  196. application.add_endpoint<ClankerSaddle.Api.ServiceWorkerEndpoint>(
  197. new EndpointRoute("/sw.js", Method.GET));
  198. GLib.Idle.add(() => {
  199. seed_admin.begin(application.container);
  200. start_dispatcher.begin(application.container);
  201. return false;
  202. });
  203. application.run();
  204. return 0;
  205. } catch (Error e) {
  206. printerr("Error: %s\n", e.message);
  207. return 1;
  208. }
  209. }
  210. /**
  211. * Seeds the admin account when BOTH SPRY_SEED_ADMIN_USERNAME and
  212. * SPRY_SEED_ADMIN_PASSWORD are set; skipped silently otherwise. The
  213. * password is never logged.
  214. */
  215. private async void seed_admin(Container container) {
  216. var username = Environment.get_variable("SPRY_SEED_ADMIN_USERNAME") ?? "";
  217. var password = Environment.get_variable("SPRY_SEED_ADMIN_PASSWORD") ?? "";
  218. if (username.length == 0 || password.length == 0) {
  219. return;
  220. }
  221. try {
  222. var scope = container.create_scope();
  223. yield SpryAuth.ensure_admin(scope.resolve<UserService>(),
  224. username, @"$(username)@example.com", password);
  225. print("[ClankerSaddle] Seeded admin account: %s\n", username);
  226. } catch (Error e) {
  227. printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message);
  228. }
  229. }
  230. private async void start_dispatcher(Container container) {
  231. try {
  232. var scope = container.create_scope();
  233. scope.resolve<Dispatcher>().start();
  234. } catch (Error e) {
  235. printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message);
  236. }
  237. }