// SchedulesPage — mirrors the Tasks 3-pane shell: list | detail | history
// Data model derives from ui/src/pages/SchedulesPage.tsx.
const DAYS = ['日', '月', '火', '水', '木', '金', '土'];
function parseCronToDisplay(cron) {
if (cron === 'once') return '一回のみ';
const parts = (cron || '').split(' ');
if (parts.length !== 5) return cron;
const [min, hour, dom, , dow] = parts;
const hhmm = `${hour}:${String(min).padStart(2, '0')}`;
if (dom !== '*' && dow === '*') return `毎月${dom}日 ${hhmm}`;
if (dow !== '*' && dom === '*') return `毎週${DAYS[Number(dow)] ?? dow}曜 ${hhmm}`;
if (dom === '*' && dow === '*') return `毎日 ${hhmm}`;
return cron;
}
function formatDateShort(iso) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleString('ja-JP', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
}
function relativeFromNow(iso) {
if (!iso) return '—';
const diff = new Date(iso).getTime() - Date.now();
const abs = Math.abs(diff);
const mins = Math.round(abs / 60000);
const hrs = Math.round(mins / 60);
const days = Math.round(hrs / 24);
const unit = mins < 60 ? `${mins}分` : hrs < 24 ? `${hrs}時間` : `${days}日`;
return diff >= 0 ? `${unit}後` : `${unit}前`;
}
// ── Left: list of schedules ──────────────────────────────────────────────
function ScheduleListItem({ sch, active, onClick }) {
return (
);
}
function ScheduleListPane({ items, activeId, onSelect, filter, setFilter, search, setSearch, onOpenCreate }) {
const filtered = items.filter(s => {
if (filter === 'active' && !s.isActive) return false;
if (filter === 'paused' && s.isActive) return false;
if (filter === 'event' && s.triggerKind !== 'event') return false;
if (search && !(s.title + s.body).toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
const counts = {
all: items.length,
active: items.filter(s => s.isActive).length,
paused: items.filter(s => !s.isActive).length,
event: items.filter(s => s.triggerKind === 'event').length,
};
const chipStyle = (on) => ({
flexShrink: 0, padding: '6px 10px', borderRadius: 9999,
fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer',
border: '1px solid ' + (on ? '#2563eb' : '#e2e8f0'),
background: on ? '#eff6ff' : '#fff',
color: on ? '#1d4ed8' : '#64748b', fontFamily: 'inherit',
});
return (
{counts.all} 件
·
{counts.active} 有効
{counts.paused > 0 && {counts.paused} 停止}
{filtered.map(s =>
onSelect(s.id)} />)}
{filtered.length === 0 && (
(search || filter !== 'all') ? (
}
title="該当するスケジュールはありません"
hint="検索やフィルタを変えてみてください。"
action={
}
/>
) : (
}
title="スケジュールがありません"
hint="定期実行やイベントトリガーを登録するとここに表示されます。"
/>
)
)}
);
}
// ── Center: schedule detail editor ───────────────────────────────────────
function FormRow({ label, help, children }) {
return (
);
}
function TextInput(props) {
return ;
}
function SelectInput({ children, ...props }) {
return ;
}
function ScheduleDetail({ sch, onPatch, onTrigger, onDelete }) {
if (!sch) {
return (
}
title="スケジュールを選択してください"
hint="左のリストから編集したいスケジュールを開きます。"
/>
);
}
const isEvent = sch.triggerKind === 'event';
return (
{/* Header */}
SCHEDULE #{sch.id}
{sch.title || 'タイトルなし'}
{/* Body */}
{/* Summary strip */}
{sch.isActive
?
: }
{/* Form card */}
基本情報
onPatch(sch.id, { title: e.target.value })} placeholder="週次ニュースまとめ" />
onPatch(sch.id, { pieceName: e.target.value })}>
onPatch(sch.id, { outputFormat: e.target.value })}>
{/* Trigger card */}
トリガー
{['cron', 'event'].map(k => (
))}
{!isEvent && (
<>
onPatch(sch.id, { cronExpression: e.target.value })}
style={{ fontFamily: 'IBM Plex Mono, monospace' }} placeholder="0 7 * * *" />
{parseCronToDisplay(sch.cronExpression)}
{sch.nextRunAt && · 次回 {formatDateShort(sch.nextRunAt)} ({relativeFromNow(sch.nextRunAt)})}
>
)}
{isEvent && (
<>
onPatch(sch.id, { eventSource: e.target.value })}>
onPatch(sch.id, { eventFilter: e.target.value })}
style={{ fontFamily: 'IBM Plex Mono, monospace' }} placeholder='repo == "agent-orchestrator" && label == "bug"' />
>
)}
{/* History */}
実行履歴
直近 {sch.history?.length || 0} 件
{(sch.history || []).length === 0 && (
}
title="まだ実行されていません"
hint={sch.isActive ? '次回の実行後にここに履歴が追加されます。' : 'スケジュールは停止中です。「再開」で有効化できます。'}
/>
)}
{(sch.history || []).map((h, i) => (
))}
);
}
function SchedulesPage({ schedules, activeId, setActiveId, onPatch, onTrigger, onDelete, onOpenCreate }) {
const [filter, setFilter] = React.useState('all');
const [search, setSearch] = React.useState('');
const active = schedules.find(s => s.id === activeId) || schedules[0];
return (
);
}
window.SchedulesPage = SchedulesPage;