moadim 1.6.0

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { Fragment } from "react";
import type { FleetRunSummary, RoutineResponse } from "../../api/hooks";
import { groupRoutines, type RCol, type RDir, type RGroupBy } from "./routineState";
import { RoutineRow } from "./RoutineRow";

function SortTh({
  label,
  col,
  current,
  dir,
  onSort,
}: {
  label: string;
  col: RCol;
  current: RCol | undefined;
  dir: RDir;
  onSort: (col: RCol) => void;
}) {
  const active = current === col;
  const indicator = active ? (dir === "asc" ? " ▲" : " ▼") : "";
  return (
    <th className={active ? "th-sort th-sort-active" : "th-sort"} onClick={() => onSort(col)}>
      {label}
      {indicator}
    </th>
  );
}

export interface RoutineTableProps {
  routines: RoutineResponse[];
  loading: boolean;
  filterActive: boolean;
  now: Date;
  selected: Set<string>;
  onSelect: (id: string) => void;
  onSelectAll: () => void;
  sortCol: RCol | undefined;
  sortDir: RDir;
  groupBy: RGroupBy;
  runHistory: Map<string, FleetRunSummary[]>;
  onSort: (col: RCol) => void;
  onEdit: (id: string) => void;
  onClone: (id: string) => void;
  onDelete: (id: string, title: string) => void;
  onToggle: (id: string, enabled: boolean) => void;
  onTrigger: (id: string) => void;
  onLogs: (id: string) => void;
  onHistory: (id: string) => void;
  onFlags: (id: string) => void;
  onClearFilters: () => void;
}

export function RoutineTable(props: RoutineTableProps) {
  const { routines, loading, filterActive, onClearFilters } = props;

  if (loading) {
    return (
      <div className="table-wrap">
        <div className="empty">
          <div className="spinner" />
        </div>
      </div>
    );
  }

  if (routines.length === 0) {
    return (
      <div className="table-wrap">
        {filterActive ? (
          <div className="empty">
            <div className="empty-icon">⊘</div>
            <div className="empty-msg">NO ROUTINES MATCH</div>
            <div className="empty-sub">
              <button type="button" className="btn btn-ghost btn-sm" onClick={onClearFilters}>
                CLEAR FILTERS
              </button>
            </div>
          </div>
        ) : (
          <div className="empty">
            <div className="empty-icon">⧗</div>
            <div className="empty-msg">NO ROUTINES SCHEDULED</div>
            <div className="empty-sub">press + NEW ROUTINE to create one</div>
          </div>
        )}
      </div>
    );
  }

  const allVisibleSelected = routines.length > 0 && routines.every((r) => props.selected.has(r.id));
  const groups = groupRoutines(routines, props.groupBy);

  return (
    <div className="table-wrap">
      <table>
        <thead>
          <tr>
            <th className="col-select">
              <input
                type="checkbox"
                checked={allVisibleSelected}
                onChange={props.onSelectAll}
                aria-label="Select all visible routines"
                title="Select all visible"
              />
            </th>
            <SortTh label="TITLE" col="title" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <th>SCHEDULE</th>
            <SortTh label="NEXT RUN" col="next_run" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <SortTh label="LAST FIRE" col="last_fire" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <th>RUN HISTORY</th>
            <SortTh label="AGENT" col="agent" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <th>REPOS</th>
            <th>MACHINES</th>
            <th>TAGS</th>
            <th>TTL</th>
            <SortTh label="HEALTH" col="health" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <SortTh label="ENABLED" col="enabled" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <SortTh label="UPDATED" col="updated" current={props.sortCol} dir={props.sortDir} onSort={props.onSort} />
            <th />
          </tr>
        </thead>
        <tbody>
          {groups.map(([label, group]) => (
            <Fragment key={`grp-${label}`}>
              {props.groupBy !== "none" && (
                <tr className="group-hd" key={`hd-${label}`}>
                  <td colSpan={15}>
                    <span className="group-label">{label}</span>{" "}
                    <span className="group-count">({group.length})</span>
                  </td>
                </tr>
              )}
              {group.map((r) => (
                <RoutineRow
                  key={r.id}
                  routine={r}
                  now={props.now}
                  runs={props.runHistory.get(r.id) ?? []}
                  selected={props.selected.has(r.id)}
                  onSelect={props.onSelect}
                  onEdit={props.onEdit}
                  onClone={props.onClone}
                  onDelete={props.onDelete}
                  onToggle={props.onToggle}
                  onTrigger={props.onTrigger}
                  onLogs={props.onLogs}
                  onHistory={props.onHistory}
                  onFlags={props.onFlags}
                />
              ))}
            </Fragment>
          ))}
        </tbody>
      </table>
    </div>
  );
}