moadim 3.2.3

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
import { useEffect, useState } from "react";
import { icalFeedUrl, type RoutineResponse } from "../../api/hooks";
import {
  CAL_MONTHS,
  GRID_CELLS,
  WEEKDAYS,
  dateOnly,
  fireTimesOnDay,
  monthStart,
  occurrencesPerDay,
  scheduleList,
} from "../../lib/schedule";
import { isRoutineSnoozed } from "./filter";
import { useToasts } from "../../shell/toasts";

export interface RoutineCalendarProps {
  routines: RoutineResponse[];
  loading: boolean;
  onEdit: (id: string) => void;
  onTrigger: (id: string) => void;
}

interface Hit {
  id: string;
  title: string;
  count: number;
  snoozed: boolean;
}

interface DayFire {
  id: string;
  title: string;
  times: Date[];
  snoozed: boolean;
}

function fmtDateLabel(day: Date): string {
  return day.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" });
}

function fmtHm(dt: Date): string {
  return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`;
}

function firesForDay(routines: RoutineResponse[], day: Date, now: Date): DayFire[] {
  return routines
    .filter((r) => r.enabled)
    .map((r) => {
      const seen = new Set<number>();
      const times = scheduleList(r)
        .flatMap((schedule) => fireTimesOnDay(schedule, day))
        .sort((a, b) => a.getTime() - b.getTime())
        .filter((fire) => {
          const ms = fire.getTime();
          if (seen.has(ms)) return false;
          seen.add(ms);
          return true;
        });
      return { id: r.id, title: r.title, times, snoozed: isRoutineSnoozed(r, now) };
    })
    .filter((fire) => fire.times.length > 0)
    .sort((a, b) => (a.times[0]?.getTime() ?? 0) - (b.times[0]?.getTime() ?? 0) || a.title.localeCompare(b.title));
}

/** Month-calendar view of upcoming routine fire times. */
export function RoutineCalendar({ routines, loading, onEdit, onTrigger }: RoutineCalendarProps) {
  const [offset, setOffset] = useState(0);
  const [selectedDay, setSelectedDay] = useState<Date | undefined>(undefined);
  const { addToast } = useToasts();

  useEffect(() => {
    if (selectedDay === undefined) return;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") setSelectedDay(undefined);
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [selectedDay]);

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

  const onSubscribe = () => {
    const url = `${window.location.origin}${icalFeedUrl()}`;
    navigator.clipboard
      .writeText(url)
      .then(() => addToast("Calendar feed URL copied", "ok"))
      .catch(() => addToast("Copy failed", "err"));
  };

  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const first = monthStart(today, offset);
  const gridStart = new Date(first);
  gridStart.setDate(gridStart.getDate() - first.getDay());

  const calNow = new Date();
  const cells: Hit[][] = Array.from({ length: GRID_CELLS }, () => []);
  let scheduled = 0;
  for (const r of routines.filter((r) => r.enabled)) {
    const countsBySchedule = scheduleList(r)
      .map((schedule) => occurrencesPerDay(schedule, gridStart))
      .filter((counts): counts is number[] => counts !== undefined);
    if (countsBySchedule.length === 0) continue;
    scheduled++;
    const snoozed = isRoutineSnoozed(r, calNow);
    const counts = countsBySchedule.reduce((acc, cur) => acc.map((v, i) => v + (cur[i] ?? 0)));
    counts.forEach((c, i) => {
      if (c > 0) cells[i]?.push({ id: r.id, title: r.title, count: c, snoozed });
    });
  }

  const monthLabel = `${CAL_MONTHS[first.getMonth()]} ${first.getFullYear()}`;
  const dayFires = selectedDay ? firesForDay(routines, selectedDay, calNow) : [];

  return (
    <div className="cal-wrap">
      <div className="cal-nav">
        <button type="button" className="btn-refresh" title="Previous month" aria-label="Previous month" onClick={() => setOffset((o) => o - 1)}>
          ‹
        </button>
        <div className="cal-month">{monthLabel}</div>
        <button type="button" className="btn-refresh" title="Next month" aria-label="Next month" onClick={() => setOffset((o) => o + 1)}>
          ›
        </button>
        <button type="button" className="btn btn-ghost btn-sm" onClick={() => setOffset(0)}>
          TODAY
        </button>
        <button type="button" className="btn btn-ghost btn-sm" title="Copy the routines.ics feed URL" onClick={onSubscribe}>
          SUBSCRIBE
        </button>
      </div>
      {scheduled === 0 ? (
        <div className="empty">
          <div className="empty-icon">🗓</div>
          <div className="empty-msg">NOTHING SCHEDULED</div>
          <div className="empty-sub">enabled routines with a valid schedule appear here</div>
        </div>
      ) : (
        <>
          <div className="cal-weekdays">
            {WEEKDAYS.map((d) => (
              <div className="cal-weekday" key={d}>
                {d}
              </div>
            ))}
          </div>
          <div className="cal-grid">
            {cells.map((hits, i) => {
              const date = new Date(gridStart);
              date.setDate(date.getDate() + i);
              const day = dateOnly(date);
              let cls = "cal-day";
              if (date.getMonth() !== first.getMonth()) cls += " other-month";
              if (date.getTime() === today.getTime()) cls += " today";
              const dateLabel = fmtDateLabel(day);
              return (
                <div
                  className={cls}
                  key={i}
                  role="button"
                  tabIndex={0}
                  aria-label={`Open schedule details for ${dateLabel}`}
                  onClick={() => setSelectedDay(day)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter" || e.key === " ") {
                      e.preventDefault();
                      setSelectedDay(day);
                    }
                  }}
                >
                  <div className="cal-daynum">{date.getDate()}</div>
                  <div className="cal-hits">
                    {hits.slice(0, 4).map((hit, idx) => {
                      const label = hit.count > 1 ? `${hit.title} ×${hit.count}` : hit.title;
                      let chipCls = "cal-chip clickable";
                      if (hit.snoozed) chipCls += " snoozed";
                      return (
                        <button
                          type="button"
                          className={chipCls}
                          aria-label={`Edit ${hit.title}`}
                          title={label}
                          key={`${hit.id}-${idx}`}
                          onClick={(e) => {
                            e.stopPropagation();
                            onEdit(hit.id);
                          }}
                        >
                          {label}
                        </button>
                      );
                    })}
                    {hits.length > 4 && <div className="cal-more">+{hits.length - 4} more</div>}
                  </div>
                </div>
              );
            })}
          </div>
        </>
      )}

      {selectedDay && (
        <div className="overlay" onClick={() => setSelectedDay(undefined)}>
          <div className="dialog cal-day-dialog" role="dialog" aria-modal="true" aria-label="Calendar day details" onClick={(e) => e.stopPropagation()}>
            <div className="dialog-title">{fmtDateLabel(selectedDay)}</div>
            <div className="dialog-msg">Scheduled routine fires for this local day.</div>
            {dayFires.length === 0 ? (
              <div className="cal-day-empty">No routine fires on this day.</div>
            ) : (
              <div className="cal-day-fire-list">
                {dayFires.map((fire) => (
                  <div className="cal-day-fire" key={fire.id}>
                    <div className="cal-day-fire-main">
                      <div className="cal-day-fire-title">{fire.title}</div>
                      <div className="cal-day-fire-times">
                        {fire.times.map((time) => (
                          <span className="cal-time-chip" key={time.getTime()}>
                            {fmtHm(time)}
                          </span>
                        ))}
                      </div>
                    </div>
                    <button
                      type="button"
                      className="act-btn run"
                      aria-label={`Run ${fire.title} now`}
                      disabled={fire.snoozed}
                      title={fire.snoozed ? "Routine is snoozed" : "Trigger this routine now"}
                      onClick={() => onTrigger(fire.id)}
                    >
                      RUN NOW
                    </button>
                  </div>
                ))}
              </div>
            )}
            <div className="dialog-actions">
              <button type="button" className="btn btn-ghost" onClick={() => setSelectedDay(undefined)}>
                Close
              </button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}