feat(dashboard): improve scheduled work UX (#21)
This commit is contained in:
@@ -28,6 +28,8 @@ type UsageRow = DashboardOverview['usage']['rows'][number];
|
||||
type RiskLevel = 'ok' | 'warn' | 'critical';
|
||||
type UsageGroup = 'primary' | 'codex';
|
||||
type DashboardView = 'usage' | 'health' | 'rooms' | 'scheduled';
|
||||
type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed';
|
||||
type TaskResultTone = 'ok' | 'fail' | 'none';
|
||||
|
||||
const REFRESH_INTERVAL_MS = 15_000;
|
||||
const LOCALE_STORAGE_KEY = 'ejclaw.dashboard.locale';
|
||||
@@ -82,6 +84,46 @@ function formatDate(value: string | null | undefined, locale: Locale): string {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatTaskDate(
|
||||
value: string | null | undefined,
|
||||
locale: Locale,
|
||||
): string {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatRelativeDate(
|
||||
value: string | null | undefined,
|
||||
locale: Locale,
|
||||
t: Messages,
|
||||
): string {
|
||||
if (!value) return t.tasks.noTime;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
const diffMs = date.getTime() - Date.now();
|
||||
const absMs = Math.abs(diffMs);
|
||||
if (absMs < 45_000) return t.tasks.now;
|
||||
|
||||
const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
|
||||
['day', 86_400_000],
|
||||
['hour', 3_600_000],
|
||||
['minute', 60_000],
|
||||
];
|
||||
const [unit, unitMs] =
|
||||
units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!;
|
||||
return new Intl.RelativeTimeFormat(localeTags[locale], {
|
||||
numeric: 'auto',
|
||||
style: 'short',
|
||||
}).format(Math.round(diffMs / unitMs), unit);
|
||||
}
|
||||
|
||||
function formatPct(value: number): string {
|
||||
if (value < 0) return '-';
|
||||
return `${Math.round(value)}%`;
|
||||
@@ -127,6 +169,52 @@ function queueLabel(
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
const SECRET_ASSIGNMENT_RE =
|
||||
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi;
|
||||
const SECRET_VALUE_RE =
|
||||
/\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g;
|
||||
|
||||
function safePreview(
|
||||
value: string | null | undefined,
|
||||
fallback: string,
|
||||
): string {
|
||||
const cleaned = (value ?? '')
|
||||
.replace(SECRET_ASSIGNMENT_RE, '$1=<redacted>')
|
||||
.replace(SECRET_VALUE_RE, '<redacted-token>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (!cleaned) return fallback;
|
||||
return cleaned.length > 120 ? `${cleaned.slice(0, 120)}...` : cleaned;
|
||||
}
|
||||
|
||||
function taskGroupKey(task: DashboardTask): TaskGroupKey {
|
||||
if (task.status === 'completed') return 'completed';
|
||||
if (task.status === 'paused') return 'paused';
|
||||
if (task.isWatcher) return 'watchers';
|
||||
return 'scheduled';
|
||||
}
|
||||
|
||||
function taskResultTone(task: DashboardTask): TaskResultTone {
|
||||
if (!task.lastResult) return 'none';
|
||||
const normalized = task.lastResult.toLowerCase();
|
||||
if (
|
||||
normalized.includes('fail') ||
|
||||
normalized.includes('error') ||
|
||||
normalized.includes('timeout') ||
|
||||
normalized.includes('cancel') ||
|
||||
normalized.includes('reject')
|
||||
) {
|
||||
return 'fail';
|
||||
}
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function taskDisplayName(task: DashboardTask, t: Messages): string {
|
||||
if (task.isWatcher) return t.tasks.ciWatch;
|
||||
if (task.scheduleType) return task.scheduleType;
|
||||
return task.id;
|
||||
}
|
||||
|
||||
function navItems(t: Messages) {
|
||||
return [
|
||||
{ href: '#/usage', label: t.nav.usage, view: 'usage' as const },
|
||||
@@ -691,111 +779,178 @@ function TaskPanel({
|
||||
locale: Locale;
|
||||
t: Messages;
|
||||
}) {
|
||||
const sortedTasks = useMemo(
|
||||
() =>
|
||||
[...tasks].sort((a, b) => {
|
||||
const statusRank = { active: 0, paused: 1, completed: 2 } as const;
|
||||
const rankDelta = statusRank[a.status] - statusRank[b.status];
|
||||
if (rankDelta !== 0) return rankDelta;
|
||||
return (a.nextRun ?? a.createdAt).localeCompare(
|
||||
b.nextRun ?? b.createdAt,
|
||||
);
|
||||
}),
|
||||
[tasks],
|
||||
);
|
||||
const taskGroups = useMemo(() => {
|
||||
const groups: Record<TaskGroupKey, DashboardTask[]> = {
|
||||
watchers: [],
|
||||
scheduled: [],
|
||||
paused: [],
|
||||
completed: [],
|
||||
};
|
||||
|
||||
if (sortedTasks.length === 0) {
|
||||
for (const task of tasks) {
|
||||
groups[taskGroupKey(task)].push(task);
|
||||
}
|
||||
|
||||
for (const groupTasks of Object.values(groups)) {
|
||||
groupTasks.sort((a, b) =>
|
||||
(a.nextRun ?? a.lastRun ?? a.createdAt).localeCompare(
|
||||
b.nextRun ?? b.lastRun ?? b.createdAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{ key: 'watchers' as const, tasks: groups.watchers },
|
||||
{ key: 'scheduled' as const, tasks: groups.scheduled },
|
||||
{ key: 'paused' as const, tasks: groups.paused },
|
||||
{ key: 'completed' as const, tasks: groups.completed },
|
||||
];
|
||||
}, [tasks]);
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return <EmptyState>{t.tasks.empty}</EmptyState>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table-wrap desktop-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t.tasks.task}</th>
|
||||
<th>{t.tasks.status}</th>
|
||||
<th>{t.tasks.schedule}</th>
|
||||
<th>{t.tasks.next}</th>
|
||||
<th>{t.tasks.last}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedTasks.map((task) => (
|
||||
<tr key={task.id}>
|
||||
<td>
|
||||
<strong>{task.isWatcher ? t.tasks.ciWatch : task.id}</strong>
|
||||
<span>{task.promptPreview || t.tasks.emptyPrompt}</span>
|
||||
<small>
|
||||
{task.groupFolder} · {task.promptLength} {t.units.chars}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`pill pill-${task.status}`}>
|
||||
{statusLabel(task.status, t)}
|
||||
</span>
|
||||
{task.suspendedUntil ? (
|
||||
<small>
|
||||
{t.tasks.until} {formatDate(task.suspendedUntil, locale)}
|
||||
</small>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
{task.scheduleType} · {task.scheduleValue}
|
||||
<small>{task.contextMode}</small>
|
||||
</td>
|
||||
<td>{formatDate(task.nextRun, locale)}</td>
|
||||
<td>
|
||||
{formatDate(task.lastRun, locale)}
|
||||
{task.lastResult ? <small>{task.lastResult}</small> : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="task-board" aria-label={t.tasks.cardsAria}>
|
||||
{taskGroups.map((group) => {
|
||||
const label = t.tasks.groups[group.key];
|
||||
const groupHead = (
|
||||
<div className="task-group-head">
|
||||
<div>
|
||||
<span className="eyebrow">{label}</span>
|
||||
<strong>
|
||||
{group.tasks.length} {t.tasks.count}
|
||||
</strong>
|
||||
</div>
|
||||
<span className={`pill pill-${group.key}`}>
|
||||
{group.tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
const groupBody =
|
||||
group.tasks.length === 0 ? (
|
||||
<div className="task-group-empty">{t.tasks.groupEmpty}</div>
|
||||
) : (
|
||||
<div className="task-list">
|
||||
{group.tasks.map((task) => {
|
||||
const resultTone = taskResultTone(task);
|
||||
const lastResult = safePreview(
|
||||
task.lastResult,
|
||||
t.tasks.noResult,
|
||||
);
|
||||
return (
|
||||
<article
|
||||
className={`task-card task-card-${group.key}`}
|
||||
key={task.id}
|
||||
>
|
||||
<div className="task-card-main">
|
||||
<div className="task-title">
|
||||
<strong>{taskDisplayName(task, t)}</strong>
|
||||
<span className="mono-chip">{task.groupFolder}</span>
|
||||
</div>
|
||||
<div className="task-status-line">
|
||||
<span className={`pill pill-${task.status}`}>
|
||||
{statusLabel(task.status, t)}
|
||||
</span>
|
||||
{task.ciProvider ? (
|
||||
<span className="task-provider">
|
||||
{task.ciProvider}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mobile-record-list" aria-label={t.tasks.cardsAria}>
|
||||
{sortedTasks.map((task) => (
|
||||
<article className="record-card task-card" key={task.id}>
|
||||
<div className="record-card-head">
|
||||
<div>
|
||||
<strong>{task.isWatcher ? t.tasks.ciWatch : task.id}</strong>
|
||||
<span className="mono-chip">{task.groupFolder}</span>
|
||||
</div>
|
||||
<span className={`pill pill-${task.status}`}>
|
||||
{statusLabel(task.status, t)}
|
||||
</span>
|
||||
<div className="task-time-grid">
|
||||
<span>
|
||||
<small>{t.tasks.next}</small>
|
||||
<strong>{formatTaskDate(task.nextRun, locale)}</strong>
|
||||
<em>{formatRelativeDate(task.nextRun, locale, t)}</em>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.last}</small>
|
||||
<strong>{formatTaskDate(task.lastRun, locale)}</strong>
|
||||
<em>{formatRelativeDate(task.lastRun, locale, t)}</em>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.schedule}</small>
|
||||
<strong>{task.scheduleType}</strong>
|
||||
<em>{task.scheduleValue}</em>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{task.suspendedUntil ? (
|
||||
<div className="task-suspended">
|
||||
<span>{t.tasks.suspendedUntil}</span>
|
||||
<strong>
|
||||
{formatTaskDate(task.suspendedUntil, locale)}
|
||||
</strong>
|
||||
<em>
|
||||
{formatRelativeDate(task.suspendedUntil, locale, t)}
|
||||
</em>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={`task-result result-${resultTone}`}>
|
||||
<span>
|
||||
{resultTone === 'fail'
|
||||
? t.tasks.resultFail
|
||||
: resultTone === 'ok'
|
||||
? t.tasks.resultOk
|
||||
: t.tasks.result}
|
||||
</span>
|
||||
<strong>{lastResult}</strong>
|
||||
</div>
|
||||
|
||||
<details className="task-prompt">
|
||||
<summary>{t.tasks.prompt}</summary>
|
||||
<p>
|
||||
{safePreview(task.promptPreview, t.tasks.emptyPrompt)}
|
||||
</p>
|
||||
<small>
|
||||
{task.id} · {task.contextMode} · {task.promptLength}{' '}
|
||||
{t.units.chars}
|
||||
</small>
|
||||
</details>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p>{task.promptPreview || t.tasks.emptyPrompt}</p>
|
||||
<div className="record-card-grid">
|
||||
<span>
|
||||
<small>{t.tasks.schedule}</small>
|
||||
<strong>{task.scheduleType}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.next}</small>
|
||||
<strong>{formatDate(task.nextRun, locale)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.last}</small>
|
||||
<strong>{formatDate(task.lastRun, locale)}</strong>
|
||||
</span>
|
||||
<span>
|
||||
<small>{t.tasks.context}</small>
|
||||
<strong>{task.contextMode}</strong>
|
||||
</span>
|
||||
</div>
|
||||
{task.lastResult ? (
|
||||
<p className="record-id">
|
||||
{t.tasks.lastResult}: {task.lastResult}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (group.key === 'completed') {
|
||||
return (
|
||||
<details
|
||||
className="task-group task-group-completed"
|
||||
key={group.key}
|
||||
>
|
||||
<summary className="task-group-head">
|
||||
<div>
|
||||
<span className="eyebrow">{label}</span>
|
||||
<strong>
|
||||
{group.tasks.length} {t.tasks.count}
|
||||
</strong>
|
||||
</div>
|
||||
<span className={`pill pill-${group.key}`}>
|
||||
{group.tasks.length}
|
||||
</span>
|
||||
</summary>
|
||||
{groupBody}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`task-group task-group-${group.key}`}
|
||||
key={group.key}
|
||||
>
|
||||
{groupHead}
|
||||
{groupBody}
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,14 @@ export interface Messages {
|
||||
tasks: {
|
||||
empty: string;
|
||||
cardsAria: string;
|
||||
groups: {
|
||||
watchers: string;
|
||||
scheduled: string;
|
||||
paused: string;
|
||||
completed: string;
|
||||
};
|
||||
count: string;
|
||||
groupEmpty: string;
|
||||
task: string;
|
||||
status: string;
|
||||
schedule: string;
|
||||
@@ -98,8 +106,16 @@ export interface Messages {
|
||||
context: string;
|
||||
ciWatch: string;
|
||||
emptyPrompt: string;
|
||||
prompt: string;
|
||||
until: string;
|
||||
suspendedUntil: string;
|
||||
lastResult: string;
|
||||
result: string;
|
||||
resultOk: string;
|
||||
resultFail: string;
|
||||
noResult: string;
|
||||
noTime: string;
|
||||
now: string;
|
||||
};
|
||||
status: {
|
||||
processing: string;
|
||||
@@ -222,6 +238,14 @@ export const messages = {
|
||||
tasks: {
|
||||
empty: '예약 작업 없음.',
|
||||
cardsAria: '예약 작업 카드',
|
||||
groups: {
|
||||
watchers: 'CI 감시',
|
||||
scheduled: '예약',
|
||||
paused: '일시정지',
|
||||
completed: '완료',
|
||||
},
|
||||
count: '개',
|
||||
groupEmpty: '해당 없음',
|
||||
task: '작업',
|
||||
status: '상태',
|
||||
schedule: '스케줄',
|
||||
@@ -230,8 +254,16 @@ export const messages = {
|
||||
context: '컨텍스트',
|
||||
ciWatch: 'CI 감시',
|
||||
emptyPrompt: '(빈 미리보기)',
|
||||
prompt: '프롬프트',
|
||||
until: '까지',
|
||||
suspendedUntil: '정지 해제',
|
||||
lastResult: '최근 결과',
|
||||
result: '결과 없음',
|
||||
resultOk: '정상',
|
||||
resultFail: '실패',
|
||||
noResult: '결과 없음',
|
||||
noTime: '-',
|
||||
now: '지금',
|
||||
},
|
||||
status: {
|
||||
processing: '처리중',
|
||||
@@ -338,6 +370,14 @@ export const messages = {
|
||||
tasks: {
|
||||
empty: 'No scheduled work.',
|
||||
cardsAria: 'Scheduled task cards',
|
||||
groups: {
|
||||
watchers: 'CI watch',
|
||||
scheduled: 'Scheduled',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
},
|
||||
count: 'items',
|
||||
groupEmpty: 'None',
|
||||
task: 'task',
|
||||
status: 'status',
|
||||
schedule: 'schedule',
|
||||
@@ -346,8 +386,16 @@ export const messages = {
|
||||
context: 'context',
|
||||
ciWatch: 'CI Watch',
|
||||
emptyPrompt: '(empty preview)',
|
||||
prompt: 'prompt',
|
||||
until: 'until',
|
||||
suspendedUntil: 'resume',
|
||||
lastResult: 'last result',
|
||||
result: 'no result',
|
||||
resultOk: 'ok',
|
||||
resultFail: 'failed',
|
||||
noResult: 'no result',
|
||||
noTime: '-',
|
||||
now: 'now',
|
||||
},
|
||||
status: {
|
||||
processing: 'processing',
|
||||
@@ -454,6 +502,14 @@ export const messages = {
|
||||
tasks: {
|
||||
empty: '暂无计划任务。',
|
||||
cardsAria: '计划任务卡片',
|
||||
groups: {
|
||||
watchers: 'CI 监控',
|
||||
scheduled: '计划',
|
||||
paused: '暂停',
|
||||
completed: '完成',
|
||||
},
|
||||
count: '项',
|
||||
groupEmpty: '无',
|
||||
task: '任务',
|
||||
status: '状态',
|
||||
schedule: '计划',
|
||||
@@ -462,8 +518,16 @@ export const messages = {
|
||||
context: '上下文',
|
||||
ciWatch: 'CI 监控',
|
||||
emptyPrompt: '(空预览)',
|
||||
prompt: '提示',
|
||||
until: '直到',
|
||||
suspendedUntil: '恢复',
|
||||
lastResult: '最近结果',
|
||||
result: '无结果',
|
||||
resultOk: '正常',
|
||||
resultFail: '失败',
|
||||
noResult: '无结果',
|
||||
noTime: '-',
|
||||
now: '现在',
|
||||
},
|
||||
status: {
|
||||
processing: '处理中',
|
||||
@@ -570,6 +634,14 @@ export const messages = {
|
||||
tasks: {
|
||||
empty: '予定作業なし。',
|
||||
cardsAria: '予定作業カード',
|
||||
groups: {
|
||||
watchers: 'CI監視',
|
||||
scheduled: '予定',
|
||||
paused: '一時停止',
|
||||
completed: '完了',
|
||||
},
|
||||
count: '件',
|
||||
groupEmpty: 'なし',
|
||||
task: '作業',
|
||||
status: '状態',
|
||||
schedule: '予定',
|
||||
@@ -578,8 +650,16 @@ export const messages = {
|
||||
context: 'コンテキスト',
|
||||
ciWatch: 'CI監視',
|
||||
emptyPrompt: '(空のプレビュー)',
|
||||
prompt: 'プロンプト',
|
||||
until: 'まで',
|
||||
suspendedUntil: '再開',
|
||||
lastResult: '直近結果',
|
||||
result: '結果なし',
|
||||
resultOk: '正常',
|
||||
resultFail: '失敗',
|
||||
noResult: '結果なし',
|
||||
noTime: '-',
|
||||
now: '今',
|
||||
},
|
||||
status: {
|
||||
processing: '処理中',
|
||||
|
||||
@@ -837,6 +837,236 @@ progress::-moz-progress-bar {
|
||||
background: rgba(255, 250, 240, 0.45);
|
||||
}
|
||||
|
||||
.task-board {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(43, 55, 38, 0.11);
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 250, 240, 0.54);
|
||||
}
|
||||
|
||||
.task-group-completed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.task-group-completed[open] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.task-group-completed summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-group-completed summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-group-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-group-head strong {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 360px));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(43, 55, 38, 0.1);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 250, 240, 0.72);
|
||||
}
|
||||
|
||||
.task-card-main,
|
||||
.task-time-grid,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-title strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.02em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-status-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-provider {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
background: rgba(43, 55, 38, 0.08);
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 9px;
|
||||
border: 1px solid rgba(43, 55, 38, 0.08);
|
||||
border-radius: 14px;
|
||||
background: rgba(43, 55, 38, 0.04);
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-suspended span,
|
||||
.task-result span,
|
||||
.task-time-grid em,
|
||||
.task-suspended em {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 780;
|
||||
}
|
||||
|
||||
.task-time-grid small,
|
||||
.task-time-grid strong,
|
||||
.task-time-grid em,
|
||||
.task-suspended span,
|
||||
.task-suspended strong,
|
||||
.task-suspended em,
|
||||
.task-result span,
|
||||
.task-result strong {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-time-grid strong,
|
||||
.task-suspended strong,
|
||||
.task-result strong {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.task-suspended {
|
||||
border-color: rgba(181, 138, 34, 0.22);
|
||||
background: rgba(181, 138, 34, 0.08);
|
||||
}
|
||||
|
||||
.task-result {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-color: rgba(63, 127, 81, 0.15);
|
||||
}
|
||||
|
||||
.result-fail {
|
||||
border-color: rgba(183, 71, 52, 0.28);
|
||||
background: rgba(183, 71, 52, 0.08);
|
||||
}
|
||||
|
||||
.result-none {
|
||||
border-color: rgba(109, 115, 95, 0.14);
|
||||
}
|
||||
|
||||
.task-prompt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.task-prompt summary {
|
||||
min-height: 32px;
|
||||
color: var(--accent-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-prompt p {
|
||||
margin: 4px 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.task-prompt small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-family:
|
||||
'SF Mono', 'Cascadia Code', 'Roboto Mono', ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-group-empty {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
color: var(--muted);
|
||||
background: rgba(43, 55, 38, 0.04);
|
||||
}
|
||||
|
||||
.pill-watchers {
|
||||
color: #173e23;
|
||||
background: rgba(63, 127, 81, 0.18);
|
||||
}
|
||||
|
||||
.pill-scheduled {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.16);
|
||||
}
|
||||
|
||||
.pill-paused {
|
||||
color: #6e4a05;
|
||||
background: rgba(181, 138, 34, 0.2);
|
||||
}
|
||||
|
||||
.pill-completed {
|
||||
color: #625d52;
|
||||
background: rgba(109, 115, 95, 0.16);
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@@ -1139,6 +1369,39 @@ progress::-moz-progress-bar {
|
||||
.record-card-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
gap: 7px;
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.task-card-main {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-time-grid > span,
|
||||
.task-suspended,
|
||||
.task-result {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.task-prompt summary {
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
@@ -1176,6 +1439,14 @@ progress::-moz-progress-bar {
|
||||
.record-card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-time-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.task-time-grid > span:last-child {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
Reference in New Issue
Block a user