main.vala 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  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. db.add_migration<ClankerSaddle.Data.Migrations.M0001_Initial>();
  27. db.add_migration<ClankerSaddle.Data.Migrations.M0002_Part2>();
  28. db.add_migration<ClankerSaddle.Data.Migrations.M0003_Rules>();
  29. db.add_migration<ClankerSaddle.Data.Migrations.M0004_ProjectNotes>();
  30. db.add_migration<ClankerSaddle.Data.Migrations.M0005_RawLog>();
  31. db.add_entity<Project>(Project.entity_mapping);
  32. db.add_entity<ProjectLink>(ProjectLink.entity_mapping);
  33. db.add_entity<TaskEntity>(TaskEntity.entity_mapping);
  34. db.add_entity<TaskDependency>(TaskDependency.entity_mapping);
  35. db.add_entity<TaskComment>(TaskComment.entity_mapping);
  36. db.add_entity<TaskBlock>(TaskBlock.entity_mapping);
  37. db.add_entity<TaskProgress>(TaskProgress.entity_mapping);
  38. db.add_entity<Saddle>(Saddle.entity_mapping);
  39. db.add_entity<Session>(Session.entity_mapping);
  40. db.add_entity<SessionCommand>(SessionCommand.entity_mapping);
  41. db.add_entity<ApiToken>(ApiToken.entity_mapping);
  42. db.add_entity<Rule>(Rule.entity_mapping);
  43. db.add_entity<RuleProject>(RuleProject.entity_mapping);
  44. db.add_entity<TaskRule>(TaskRule.entity_mapping);
  45. db.add_entity<ProjectNote>(ProjectNote.entity_mapping);
  46. db.add_entity<SessionRawLog>(SessionRawLog.entity_mapping);
  47. db.migrate_on_startup();
  48. application.add_scoped<ProjectService>();
  49. application.add_scoped<TaskService>();
  50. application.add_scoped<SaddleService>();
  51. application.add_scoped<RuleService>();
  52. application.add_singleton<SaddlePollState>();
  53. application.add_singleton<StatePulse>();
  54. application.add_singleton<Dispatcher>();
  55. var statum = application.configure_with<StatumConfigurator>();
  56. statum.global<ClankerSaddle.Services.PulsePublic>(StatePulse.SLOT_TYPE,
  57. new ClankerSaddle.Services.PulsePublic());
  58. // spry:pages-begin
  59. statum.add_page<HomePage, HomeEntrypoint>();
  60. statum.add_page<LoginPage, LoginEntrypoint>();
  61. statum.add_page<ProjectsPage, ProjectsEntrypoint>();
  62. statum.add_page<ProjectDetailPage, ProjectDetailEntrypoint>();
  63. statum.add_page<TasksPage, TasksEntrypoint>();
  64. statum.add_page<TaskDetailPage, TaskDetailEntrypoint>();
  65. statum.add_page<TaskTerminalPage, TaskTerminalEntrypoint>();
  66. statum.add_page<SaddlesPage, SaddlesEntrypoint>();
  67. statum.add_page<RulesPage, RulesEntrypoint>();
  68. // spry:pages-end
  69. // spry:actions-begin
  70. statum.action<LoginAction>();
  71. statum.action<Spry.Actions.LogoutAction>();
  72. statum.action<CreateProjectAction>();
  73. statum.action<UpdateProjectAction>();
  74. statum.action<AddProjectLinkAction>();
  75. statum.action<RemoveProjectLinkAction>();
  76. statum.action<CreateTaskAction>();
  77. statum.action<UpdateTaskAction>();
  78. statum.action<AddCommentAction>();
  79. statum.action<UpdatePlanAction>();
  80. statum.action<AnswerBlockAction>();
  81. statum.action<CreateSubtaskAction>();
  82. statum.action<AddDependencyAction>();
  83. statum.action<CompleteTaskAction>();
  84. statum.action<CancelTaskAction>();
  85. statum.action<RefreshTaskDataAction>();
  86. statum.action<RefreshDashboardAction>();
  87. statum.action<RegisterSaddleAction>();
  88. statum.action<UnregisterSaddleAction>();
  89. statum.action<RefreshSaddleStatusAction>();
  90. statum.action<CreateRuleAction>();
  91. statum.action<UpdateRuleAction>();
  92. statum.action<DeleteRuleAction>();
  93. statum.action<AttachProjectRuleAction>();
  94. statum.action<DetachProjectRuleAction>();
  95. statum.action<AttachTaskRuleAction>();
  96. statum.action<DetachTaskRuleAction>();
  97. statum.action<DeleteProjectNoteAction>();
  98. // spry:actions-end
  99. // spry:resources-begin
  100. statum.add_resource<ClankerSaddle.AppCss>();
  101. // spry:resources-end
  102. // spry:auth-begin
  103. // spry:auth-end
  104. application.add_endpoint<SaddleUpdateEndpoint>(
  105. new EndpointRoute("/api/saddle/{id}/update", Method.POST));
  106. application.add_endpoint<SaddlePollEndpoint>(
  107. new EndpointRoute("/api/saddle/{id}/poll", Method.GET));
  108. application.add_endpoint<SaddleNoteEndpoint>(
  109. new EndpointRoute("/api/saddle/note", Method.POST));
  110. application.add_endpoint<ClankerSaddle.Api.TerminalSseEndpoint>(
  111. new EndpointRoute("/terminal-stream/{session_id}", Method.GET));
  112. GLib.Idle.add(() => {
  113. seed_admin.begin(application.container);
  114. mint_callback_token.begin(application.container);
  115. start_dispatcher.begin(application.container);
  116. return false;
  117. });
  118. application.run();
  119. return 0;
  120. } catch (Error e) {
  121. printerr("Error: %s\n", e.message);
  122. return 1;
  123. }
  124. }
  125. /**
  126. * Seeds the admin account when BOTH SPRY_SEED_ADMIN_USERNAME and
  127. * SPRY_SEED_ADMIN_PASSWORD are set; skipped silently otherwise. The
  128. * password is never logged.
  129. */
  130. private async void seed_admin(Container container) {
  131. var username = Environment.get_variable("SPRY_SEED_ADMIN_USERNAME") ?? "";
  132. var password = Environment.get_variable("SPRY_SEED_ADMIN_PASSWORD") ?? "";
  133. if (username.length == 0 || password.length == 0) {
  134. return;
  135. }
  136. try {
  137. var scope = container.create_scope();
  138. yield SpryAuth.ensure_admin(scope.resolve<UserService>(),
  139. username, @"$(username)@example.com", password);
  140. print("[ClankerSaddle] Seeded admin account: %s\n", username);
  141. } catch (Error e) {
  142. printerr("[ClankerSaddle] Admin seeding failed: %s\n", e.message);
  143. }
  144. }
  145. private async void start_dispatcher(Container container) {
  146. try {
  147. var scope = container.create_scope();
  148. scope.resolve<Dispatcher>().start();
  149. } catch (Error e) {
  150. printerr("[ClankerSaddle] Dispatcher startup failed: %s\n", e.message);
  151. }
  152. }
  153. /**
  154. * Mints the dedicated 'saddle-callback' token at startup when absent —
  155. * the credential daemons use to POST `saddle note` remarks back (see
  156. * {@link ClankerSaddle.Api.SaddleNoteEndpoint}).
  157. */
  158. private async void mint_callback_token(Container container) {
  159. try {
  160. var scope = container.create_scope();
  161. string token = yield scope.resolve<SaddleService>().ensure_callback_token();
  162. print("[ClankerSaddle] Saddle callback token ready (%s…)\n", token.substring(0, 16));
  163. } catch (Error e) {
  164. printerr("[ClankerSaddle] Callback token minting failed: %s\n", e.message);
  165. }
  166. }