| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using Spry;
- using Spry.Authentication;
- using Statum;
- using ClankerSaddle.Data;
- using ClankerSaddle.Services;
- namespace ClankerSaddle {
- /** "3m ago"-style compact duration for the health panel. */
- private static string relative_time(int64 delta_ms) {
- int64 seconds = delta_ms.clamp(0, int64.MAX) / 1000;
- if (seconds < 60) {
- return @"$(seconds)s";
- }
- if (seconds < 3600) {
- return @"$(seconds / 60)m";
- }
- return @"$(seconds / 3600)h";
- }
- /** A capability chip on a saddle card, with its remove action. */
- public class SaddleCapabilityView : Object {
- public int64 link_id { get; set; }
- public string name { get; set; default = ""; }
- public Statum.Model.ActionDto? detach { get; set; }
- }
- /** A saddle card on the management page, with its current task and API token. */
- public class SaddleRowView : Object {
- public int64 id { get; set; }
- public string name { get; set; default = ""; }
- public string host { get; set; default = ""; }
- public string status { get; set; default = ""; }
- public string health { get; set; default = "unknown"; }
- public string daemon_state { get; set; default = ""; }
- public string uptime_display { get; set; default = ""; }
- public string last_contact_display { get; set; default = ""; }
- public string git_name { get; set; default = ""; }
- public string git_email { get; set; default = ""; }
- public string ssh_public_key { get; set; default = ""; }
- public Statum.Model.ActionDto? update_identity { get; set; }
- public int64 current_task_id { get; set; }
- public string current_task_title { get; set; default = ""; }
- /** Set when the current task is a goal agent's run: link the goal instead. */
- public int64 current_goal_id { get; set; }
- public string current_goal_title { get; set; default = ""; }
- public string allocated_display { get; set; default = ""; }
- public string token { get; set; default = ""; }
- public Statum.Model.ActionDto? unregister { get; set; }
- public Statum.Model.ActionDto? refresh { get; set; }
- public Statum.Model.ActionDto? attach_capability { get; set; }
- }
- /** A capability row in the saddles page's capabilities section. */
- public class CapabilityRowView : Object {
- public int64 id { get; set; }
- public string name { get; set; default = ""; }
- public int64 saddle_count { get; set; }
- public int64 task_count { get; set; }
- public Statum.Model.ActionDto? delete { get; set; }
- }
- /** An option in the attach selects (saddle cards and the task page). */
- public class CapabilityOptionView : Object {
- public int64 id { get; set; }
- public string name { get; set; default = ""; }
- }
- /** Public data of the `saddles` slot. */
- public class SaddlesPublic : Object {
- /** True when the viewer may see the admin controls (the actions
- * re-check the permission; this only shapes the page). */
- public bool is_admin { get; set; }
- public Statum.Model.ActionDto register { get; set; }
- public Statum.Model.ActionDto create_capability { get; set; }
- }
- /**
- * Entrypoint for /saddles: the saddle inventory plus the capability
- * tag catalogue. Admin-only (the authoritative guard); each card
- * carries its current task, capability chips (with remove actions),
- * the attach-capability select, and the unregister/refresh actions.
- */
- public class SaddlesEntrypoint : StatumEntrypoint {
- private SaddleService saddle_service = Inversion.inject<SaddleService>();
- private SaddlePollState poll_state = Inversion.inject<SaddlePollState>();
- private CapabilityService capability_service = Inversion.inject<CapabilityService>();
- private InvercargillSql.Orm.OrmSession db = Inversion.inject<InvercargillSql.Orm.OrmSession>();
- private UserService users = Inversion.inject<UserService>();
- public override async DirectiveBuilder handle() throws GLib.Error {
- // Viewing is open to every logged-in user (the roles
- // decision); the page's controls are admin-only, so the
- // public slot carries the admin flag the template's stm-if
- // guards bind against. The actions themselves enforce the
- // permission server-side.
- var guard = yield SpryAuth.require_login(directives(), request, users);
- if (guard != null) {
- return (!)guard;
- }
- bool is_admin = SpryAuth.current(request).has_permission(SpryAuth.ADMIN_PERMISSION);
- var capabilities = yield capability_service.list_capabilities();
- var capability_names = new Dictionary<string, string>();
- foreach (var capability in capabilities) {
- capability_names.set(capability.id.to_string(), capability.name);
- }
- var saddle_links = yield capability_service.all_saddle_attachments();
- var task_links = yield capability_service.all_task_attachments();
- var options = new Vector<CapabilityOptionView>();
- foreach (var capability in capabilities) {
- options.add(new CapabilityOptionView() {
- id = capability.id,
- name = capability.name
- });
- }
- // Per-saddle chips keyed by saddle id, attached to the row
- // models via list_rich below (slot lists cannot nest).
- var chips_by_saddle = new Dictionary<string, Vector<SaddleCapabilityView>>();
- foreach (var link in saddle_links) {
- string name = capability_names.get_or_default(link.capability_id.to_string()) ?? "?";
- var sealed_link = new SaddleCapabilityActionPrivate() {
- saddle_id = link.saddle_id,
- link_id = link.id
- };
- var chips = chips_by_saddle.get_or_default(link.saddle_id.to_string());
- if (chips == null) {
- chips = new Vector<SaddleCapabilityView>();
- chips_by_saddle.set(link.saddle_id.to_string(), chips);
- }
- ((!)chips).add(new SaddleCapabilityView() {
- link_id = link.id,
- name = name,
- detach = action_registry.author_private<DetachSaddleCapabilityAction, SaddleCapabilityActionPrivate>(sealed_link)
- });
- }
- var rows = new Vector<SaddleRowView>();
- int64 now_ms = get_monotonic_time() / 1000;
- foreach (var saddle in yield saddle_service.list_saddles()) {
- string daemon_state = "";
- int64 uptime = 0;
- int64 activity = 0;
- int64 contact_ms = 0;
- poll_state.health_for(saddle.id, out daemon_state, out uptime,
- out activity, out contact_ms);
- string health = "unknown";
- if (saddle.status == SADDLE_OFFLINE) {
- health = "offline";
- } else if (daemon_state.length > 0) {
- health = "healthy";
- if (daemon_state == "working" && activity > 0
- && now_ms / 1000 - activity
- >= SaddleService.stale_after_seconds()) {
- health = "stale";
- }
- }
- string last_contact = contact_ms > 0
- ? relative_time(now_ms - contact_ms) : "never";
- var sealed = new SaddleActionPrivate() { saddle_id = saddle.id };
- // Plan 16 item 23: the row reads the DERIVED view —
- // busy iff a running session exists (its started_at is
- // the allocation stamp), else the stored OFFLINE probe
- // state.
- var work = yield saddle_service.current_work_view(saddle);
- var view = new SaddleRowView() {
- id = saddle.id,
- name = saddle.name,
- host = saddle.host,
- status = SaddleService.derived_status(saddle, work),
- health = health,
- daemon_state = daemon_state,
- uptime_display = uptime > 0 ? relative_time(uptime * 1000) : "",
- last_contact_display = last_contact,
- git_name = saddle.git_name ?? "",
- git_email = saddle.git_email ?? "",
- ssh_public_key = saddle.ssh_public_key ?? "",
- update_identity = action_registry.author_private<UpdateSaddleIdentityAction, SaddleActionPrivate>(sealed),
- current_task_id = work != null ? ((!)work).task_id : 0,
- allocated_display = work != null
- ? format_stamp(((!)work).started_at) : "",
- unregister = action_registry.author_private<UnregisterSaddleAction, SaddleActionPrivate>(sealed),
- refresh = action_registry.author_private<RefreshSaddleStatusAction, SaddleActionPrivate>(sealed),
- attach_capability = action_registry.author_private<AttachSaddleCapabilityAction, SaddleActionPrivate>(sealed)
- };
- view.token = saddle.token ?? "";
- if (work != null) {
- view.current_task_title = ((!)work).task_title;
- if (((!)work).goal_id > 0) {
- view.current_goal_id = ((!)work).goal_id;
- view.current_goal_title = ((!)work).goal_title;
- }
- }
- rows.add(view);
- }
- var capability_rows = new Vector<CapabilityRowView>();
- foreach (var capability in capabilities) {
- int64 saddle_count = 0;
- foreach (var link in saddle_links) {
- if (link.capability_id == capability.id) {
- saddle_count++;
- }
- }
- int64 task_count = 0;
- foreach (var link in task_links) {
- if (link.capability_id == capability.id) {
- task_count++;
- }
- }
- var sealed_capability = new CapabilityActionPrivate() { capability_id = capability.id };
- capability_rows.add(new CapabilityRowView() {
- id = capability.id,
- name = capability.name,
- saddle_count = saddle_count,
- task_count = task_count,
- delete = action_registry.author_private<DeleteCapabilityAction, CapabilityActionPrivate>(sealed_capability)
- });
- }
- var pub = new SaddlesPublic();
- pub.is_admin = is_admin;
- pub.register = action_registry.author<RegisterSaddleAction>();
- pub.create_capability = action_registry.author<CreateCapabilityAction>();
- var props = (PropertyDictionary) GObjectMapping.to_properties(pub);
- props["saddles"] = StateJson.list_rich<SaddleRowView>(rows.to_immutable_buffer(),
- (item, dict) => {
- var chips = chips_by_saddle.get_or_default(((SaddleRowView) item).id.to_string());
- dict["capabilities"] = chips != null
- ? StateJson.list<SaddleCapabilityView>(((!)chips).to_immutable_buffer())
- : StateJson.list<SaddleCapabilityView>(
- new Vector<SaddleCapabilityView>().to_immutable_buffer());
- });
- props["capabilities"] = StateJson.list<CapabilityRowView>(capability_rows.to_immutable_buffer());
- props["capability_options"] = StateJson.list<CapabilityOptionView>(options.to_immutable_buffer());
- var slot = state_service.new_slot(Scope.PAGE, new State() {
- type_name = "saddles",
- public_data = props,
- private_data = new PropertyDictionary()
- });
- return directives().set(slot);
- }
- }
- }
|