Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

264 lines
8.6 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { IframeBridge, callHandlerJson, type HandlerMethod } from '@alga-psa/extension-iframe-sdk';
import { Button, Card, Stack, Text } from '@alga-psa/ui-kit';
type ScheduleRecord = {
id: string;
name?: string | null;
endpointMethod: string;
endpointPath: string;
cron: string;
timezone: string;
enabled: boolean;
lastRunAt?: string | null;
};
type ApiResult = {
ok: boolean;
status?: number;
data?: unknown;
error?: string;
};
const bridge = new IframeBridge({ devAllowWildcard: true });
bridge.ready();
const themeFallback = {
'--alga-bg': '#ffffff',
'--alga-fg': '#111827',
'--alga-border': '#e5e7eb',
'--alga-muted': '#f3f4f6',
'--alga-muted-fg': '#6b7280',
'--alga-primary': '#2563eb',
'--alga-primary-foreground': '#ffffff',
'--alga-danger': '#dc2626',
'--alga-radius': '8px',
} as React.CSSProperties;
function asErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
function addLog(
prev: string[],
message: string,
type: 'info' | 'error' | 'success' = 'info',
): string[] {
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
const prefix = type === 'error' ? '✗' : type === 'success' ? '✓' : '→';
return [`[${timestamp}] ${prefix} ${message}`, ...prev];
}
async function apiCall(method: HandlerMethod, path: string): Promise<ApiResult> {
try {
const data = await callHandlerJson(bridge, path, { method });
return { ok: true, status: 200, data: data ?? {} };
} catch (err) {
const error = asErrorMessage(err);
const statusMatch = error.match(/^Proxy error (\d+):\s*/);
if (statusMatch) {
return { ok: false, status: Number(statusMatch[1]), data: { error } };
}
return { ok: false, error };
}
}
function SchedulerDemoApp() {
const [logs, setLogs] = useState<string[]>(['Ready.']);
const [schedules, setSchedules] = useState<ScheduleRecord[]>([]);
const [loadingSchedules, setLoadingSchedules] = useState(false);
const [setupBusy, setSetupBusy] = useState(false);
const outputText = useMemo(() => logs.join('\n'), [logs]);
const runSetup = async () => {
setSetupBusy(true);
setLogs((prev) => addLog(prev, 'Running schedule setup...'));
try {
const result = await apiCall('POST', '/api/setup');
if (result.ok) {
const payload = result.data as { results?: unknown };
setLogs((prev) =>
addLog(prev, `Setup completed: ${JSON.stringify(payload?.results, null, 2)}`, 'success'),
);
await loadSchedules();
return;
}
setLogs((prev) => addLog(prev, `Setup failed: ${JSON.stringify(result.data ?? result.error)}`, 'error'));
} finally {
setSetupBusy(false);
}
};
const loadSchedules = async () => {
setLoadingSchedules(true);
setLogs((prev) => addLog(prev, 'Loading schedules...'));
try {
const result = await apiCall('GET', '/api/schedules');
const payload = result.data as { schedules?: ScheduleRecord[] } | undefined;
const nextSchedules = payload?.schedules ?? [];
if (result.ok && Array.isArray(payload?.schedules)) {
setSchedules(nextSchedules);
if (nextSchedules.length === 0) {
setLogs((prev) => addLog(prev, 'No schedules found'));
} else {
setLogs((prev) => addLog(prev, `Loaded ${nextSchedules.length} schedule(s)`, 'success'));
}
return;
}
setLogs((prev) => addLog(prev, `Failed to load schedules: ${JSON.stringify(result.data ?? result.error)}`, 'error'));
} finally {
setLoadingSchedules(false);
}
};
const deleteSchedule = async (scheduleId: string) => {
if (!window.confirm('Delete this schedule?')) return;
setLogs((prev) => addLog(prev, `Deleting schedule ${scheduleId}...`));
const result = await apiCall('DELETE', `/api/schedules/${scheduleId}`);
if (result.ok) {
setLogs((prev) => addLog(prev, 'Schedule deleted', 'success'));
await loadSchedules();
return;
}
setLogs((prev) => addLog(prev, `Delete failed: ${JSON.stringify(result.data ?? result.error)}`, 'error'));
};
const checkStatus = async () => {
setLogs((prev) => addLog(prev, 'Checking status...'));
const result = await apiCall('GET', '/api/status');
if (result.ok) {
setLogs((prev) => addLog(prev, `Status: ${JSON.stringify(result.data, null, 2)}`, 'success'));
return;
}
setLogs((prev) => addLog(prev, `Status check failed: ${JSON.stringify(result.data ?? result.error)}`, 'error'));
};
return (
<div
style={{
...themeFallback,
maxWidth: 900,
margin: '0 auto',
padding: '20px',
fontFamily:
'Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif',
color: 'var(--alga-fg)',
}}
>
<Stack gap={8} style={{ marginBottom: 20 }}>
<Text as="strong" size="lg" weight={700}>
Scheduler Demo
</Text>
<Text tone="muted">
Demonstrates the cap:scheduler.manage capability for extensions
</Text>
</Stack>
<Stack gap={16}>
<Card>
<Stack direction="row" gap={8} style={{ flexWrap: 'wrap' }}>
<Button onClick={runSetup} disabled={setupBusy}>
{setupBusy ? 'Setting up...' : 'Setup Schedules'}
</Button>
<Button variant="secondary" onClick={loadSchedules} disabled={loadingSchedules}>
{loadingSchedules ? 'Loading...' : 'Refresh List'}
</Button>
<Button variant="secondary" onClick={checkStatus}>
Check Status
</Button>
</Stack>
</Card>
<Card>
<Stack gap={12}>
<Text as="strong" weight={600}>
Current Schedules
</Text>
{loadingSchedules && <Text tone="muted">Loading...</Text>}
{!loadingSchedules && schedules.length === 0 && (
<Text tone="muted">No schedules found. Click "Setup Schedules" to create some.</Text>
)}
{!loadingSchedules && schedules.length > 0 && (
<Stack gap={10}>
{schedules.map((schedule) => (
<Card
key={schedule.id}
style={{ padding: 12, background: 'var(--alga-muted)' }}
>
<Stack gap={8}>
<Stack
direction="row"
justify="space-between"
align="center"
style={{ gap: 12, flexWrap: 'wrap' }}
>
<Stack gap={6}>
<Text as="strong" weight={600}>
{schedule.name || 'Unnamed Schedule'}
</Text>
<Text tone="muted" size="sm">
{schedule.endpointMethod} {schedule.endpointPath} {schedule.cron} ({schedule.timezone})
</Text>
<Text
size="sm"
style={{ color: schedule.enabled ? '#15803d' : 'var(--alga-muted-fg)' }}
>
{schedule.enabled ? 'Enabled' : 'Disabled'}
{schedule.lastRunAt ? ` • Last run: ${schedule.lastRunAt}` : ''}
</Text>
</Stack>
<Button variant="danger" onClick={() => void deleteSchedule(schedule.id)}>
Delete
</Button>
</Stack>
</Stack>
</Card>
))}
</Stack>
)}
</Stack>
</Card>
<Card>
<Stack gap={8}>
<Text as="strong" weight={600}>
Output
</Text>
<pre
style={{
margin: 0,
padding: '14px 16px',
borderRadius: 8,
background: '#111827',
color: '#86efac',
fontSize: 12,
lineHeight: '18px',
maxHeight: 280,
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{outputText}
</pre>
</Stack>
</Card>
</Stack>
</div>
);
}
const mountEl = document.getElementById('app');
if (!mountEl) {
throw new Error('Missing #app mount element');
}
createRoot(mountEl).render(<SchedulerDemoApp />);