chore: final pre-submission polish for JOSS

- Add CITATION.cff for GitHub citation support and Zenodo/JOSS integration
- Fix ossim reference URL in references.bib (was pointing to unrelated repo)
- Remove broken favicon reference from index.html (no public/ directory exists)
- Add meta description to index.html
- Fix sidebar title inconsistency: "Process Simulator" → "Process Scheduler Simulator"
- Rebuild dist.tar.gz with production assets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 4.6 committed 2026-03-23 19:50:45 -03:00
1 parent fa009102eb
commit 22925578b1
34 files changed
+304 -255

No files matched your search

+11 -11
View File
@@ -15,10 +15,10 @@ const SPEED_LABELS: Record<number, string> = {
};
const statusLabel: Record<SimStatus, string> = {
idle: 'Pronto',
paused: 'Pausado',
running: 'Executando',
finished: 'Concluído',
idle: 'Ready',
paused: 'Paused',
running: 'Running',
finished: 'Finished',
};
const statusDotClass: Record<SimStatus, string> = {
@@ -68,7 +68,7 @@ const ControlBar = () => {
<div
className="h-16 flex items-center px-5 gap-3 bg-slate-800 border-b border-slate-700 shadow-sm"
role="toolbar"
aria-label="Controles da simulação"
aria-label="Simulation controls"
>
{/* Left group: simulation controls */}
<div className="flex items-center gap-1.5">
@@ -85,7 +85,7 @@ const ControlBar = () => {
{/* Step */}
<IconButton
label="Avançar 1 tick (Step)"
label="Advance 1 tick (Step)"
variant="default"
onClick={handleStep}
disabled={isFinished || processes.length === 0 || isRunning}
@@ -97,7 +97,7 @@ const ControlBar = () => {
{/* Play / Pause */}
{isRunning ? (
<IconButton
label="Pausar simulação"
label="Pause simulation"
variant="primary"
onClick={pause}
data-testid="btn-pause"
@@ -106,7 +106,7 @@ const ControlBar = () => {
</IconButton>
) : (
<IconButton
label="Iniciar / Retomar simulação"
label="Start / Resume simulation"
variant="primary"
onClick={handlePlay}
disabled={!canPlay && !isRunning}
@@ -124,7 +124,7 @@ const ControlBar = () => {
<div
className="flex items-center gap-2 px-3 py-1 rounded-md bg-slate-900 border border-slate-700"
aria-live="polite"
aria-label={`Tick atual: ${currentTick}`}
aria-label={`Current tick: ${currentTick}`}
>
<span className="text-[10px] text-slate-500 uppercase tracking-wide font-medium">Tick</span>
<span
@@ -145,14 +145,14 @@ const ControlBar = () => {
<div className="ml-auto flex items-center gap-2">
<SkipBack size={14} className="text-slate-500" aria-hidden="true" />
<label htmlFor="speed-select" className="text-xs text-slate-400 sr-only">
Velocidade
Speed
</label>
<select
id="speed-select"
value={speed}
onChange={e => setSpeed(Number(e.target.value))}
className="rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Velocidade de simulação"
aria-label="Simulation speed"
data-testid="speed-select"
>
{SPEED_OPTIONS.map(s => (
+1 -1
View File
@@ -27,7 +27,7 @@ const GanttChart = () => {
return (
<div className="border-b border-slate-800 flex items-center justify-center text-slate-600 bg-slate-900/50 text-sm select-none"
style={{ minHeight: 80 }}>
Inicie a simulação para ver o diagrama de Gantt
Start the simulation to see the Gantt chart
</div>
);
}
+9 -9
View File
@@ -66,7 +66,7 @@ const AppHeader = () => {
<button
onClick={() => setPresetsOpen(o => !o)}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors"
aria-label="Carregar cenário pré-definido"
aria-label="Load preset scenario"
>
<BookOpen size={13} />
Presets
@@ -83,7 +83,7 @@ const AppHeader = () => {
{/* Dropdown */}
<div className="absolute right-0 top-full mt-1 w-72 bg-slate-800 border border-slate-600 rounded-lg shadow-xl z-50 overflow-hidden flex flex-col max-h-[min(32rem,calc(100vh-4rem))]">
<div className="px-3 py-2 border-b border-slate-700 text-[10px] text-slate-400 uppercase tracking-wider font-medium shrink-0">
Cenários pré-definidos
Preset Scenarios
</div>
<div className="overflow-y-auto">
{presets.map((preset, i) => (
@@ -112,16 +112,16 @@ const AppHeader = () => {
{/* Import JSON */}
<label
htmlFor="import-json"
title={isSimulationActive ? 'Reinicie a simulação antes de importar' : 'Importar configuração JSON'}
title={isSimulationActive ? 'Reset the simulation before importing' : 'Import JSON configuration'}
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md transition-colors cursor-pointer ${
isSimulationActive
? 'text-slate-600 cursor-not-allowed'
: 'text-slate-300 hover:text-white hover:bg-slate-700'
}`}
aria-label="Importar configuração JSON"
aria-label="Import JSON configuration"
>
<Upload size={13} />
Importar
Import
</label>
<input
id="import-json"
@@ -131,19 +131,19 @@ const AppHeader = () => {
className="sr-only"
onChange={handleImportJSON}
disabled={isSimulationActive}
aria-label="Selecionar arquivo JSON para importar"
aria-label="Select JSON file to import"
/>
{/* Export JSON */}
<button
onClick={handleExportJSON}
disabled={processes.length === 0}
title={processes.length === 0 ? 'Adicione processos antes de exportar' : 'Exportar configuração como JSON'}
title={processes.length === 0 ? 'Add processes before exporting' : 'Export configuration as JSON'}
className="flex items-center gap-1.5 text-xs text-slate-300 hover:text-white px-2.5 py-1.5 rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar configuração JSON"
aria-label="Export JSON configuration"
>
<Download size={13} />
Exportar
Export
</button>
</header>
);
+3 -3
View File
@@ -5,8 +5,8 @@ const Sidebar = () => {
return (
<aside className="w-80 min-w-[20rem] h-full bg-slate-800 border-r border-slate-700 flex flex-col shadow-xl z-20">
<div className="h-16 flex items-center px-6 border-b border-slate-700 bg-slate-800/80 backdrop-blur-sm">
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
Process Simulator
<h1 className="text-xl font-bold bg-linear-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent truncate">
Process Scheduler Simulator
</h1>
</div>
@@ -18,7 +18,7 @@ const Sidebar = () => {
</div>
<div className="h-12 border-t border-slate-700 flex items-center justify-center text-xs text-slate-500">
PSS v1.0
PSS v1.2.0
</div>
</aside>
);
@@ -70,7 +70,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard
label="Avg Response Time"
value={avgResponseTime.toFixed(2)}
sub="unidades de tempo"
sub="time units"
accentColor="#f59e0b"
accentClass="text-amber-300"
icon={<Clock size={13} />}
@@ -79,7 +79,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard
label="Avg Turnaround Time"
value={avgTurnaroundTime.toFixed(2)}
sub="unidades de tempo"
sub="time units"
accentColor="#60a5fa"
accentClass="text-blue-300"
icon={<Timer size={13} />}
@@ -88,7 +88,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard
label="Avg Waiting Time"
value={avgWaitingTime.toFixed(2)}
sub="unidades de tempo"
sub="time units"
accentColor="#fb923c"
accentClass="text-orange-300"
icon={<Hourglass size={13} />}
@@ -97,7 +97,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<MetricCard
label="CPU Throughput"
value={cpuThroughput.toFixed(3)}
sub="processos / u.t."
sub="processes / t.u."
accentColor="#34d399"
accentClass="text-emerald-300"
icon={<Zap size={13} />}
@@ -120,7 +120,7 @@ const GlobalMetrics = ({ metrics }: GlobalMetricsProps) => {
<span className="text-2xl font-bold font-mono leading-none text-violet-300">
{utilizationPct.toFixed(1)}%
</span>
<span className="text-[10px] text-slate-500">do tempo total em CPU</span>
<span className="text-[10px] text-slate-500">of total CPU time</span>
<div className="mt-1 h-2 w-full bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
+35 -9
View File
@@ -32,7 +32,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Nenhum dado disponível.
No data available.
</div>
);
}
@@ -40,6 +40,35 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
const getName = (id: string) => processMap[id]?.name ?? id;
const getColor = (id: string) => processMap[id]?.color ?? '#94a3b8';
// Reverse map: process name → process (for radar tooltip lookup)
const nameToProcess = Object.fromEntries(
Object.values(processMap).map(p => [p.name, p])
);
const radarTooltipContent = ({ active, payload, label }: { active?: boolean; payload?: readonly any[]; label?: string | number }) => {
if (!active || !payload?.length) return null;
return (
<div style={{ ...TOOLTIP_STYLE, padding: '8px 12px' }}>
<p style={{ fontSize: '10px', color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 600, marginBottom: 4 }}>
{label}
</p>
{payload.map((entry: any) => {
const proc = nameToProcess[entry.name];
const displayName = proc?.pid != null ? `#${proc.pid} ${proc.name}` : entry.name;
return (
<div key={entry.name} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '11px', marginBottom: 2 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', backgroundColor: entry.color, flexShrink: 0, display: 'inline-block' }} />
<span style={{ color: '#cbd5e1' }}>{displayName}</span>
<span style={{ marginLeft: 'auto', paddingLeft: 12, fontFamily: 'monospace', color: '#e2e8f0' }}>
{typeof entry.value === 'number' ? `${entry.value.toFixed(1)}%` : '-'}
</span>
</div>
);
})}
</div>
);
};
// ── Stacked bar data ──────────────────────────────────────────
const barData = metrics.map((m) => ({
name: getName(m.processId),
@@ -87,9 +116,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
{showRadar && (
<div className="flex-1 min-w-0">
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Perfil Comparativo (normalizado)
Comparative Profile (normalized)
</p>
<ResponsiveContainer width="100%" height={220}>
<ResponsiveContainer width="100%" height={400}>
<RadarChart data={radarData} margin={{ top: 8, right: 20, bottom: 8, left: 20 }}>
<PolarGrid stroke="#334155" />
<PolarAngleAxis
@@ -108,10 +137,7 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
dot={{ r: 3, fill: getColor(m.processId) }}
/>
))}
<Tooltip
contentStyle={TOOLTIP_STYLE}
formatter={(value) => [typeof value === 'number' ? `${value.toFixed(1)}%` : '-', '']}
/>
<Tooltip content={radarTooltipContent} />
<Legend
wrapperStyle={{ fontSize: '10px', paddingTop: '4px', color: '#94a3b8' }}
/>
@@ -123,9 +149,9 @@ const MetricsChart = ({ metrics, processMap }: MetricsChartProps) => {
{/* ── Stacked bar chart ─────────────────────────────────── */}
<div className={showRadar ? 'flex-1 min-w-0' : 'w-full'}>
<p className="text-[10px] text-slate-400 uppercase tracking-wider mb-1 font-medium text-center">
Breakdown por Processo (ticks)
Breakdown per Process (ticks)
</p>
<ResponsiveContainer width="100%" height={220}>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={barData} margin={{ top: 8, right: 12, left: -8, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis
+8 -8
View File
@@ -8,12 +8,12 @@ import MetricsTable from './MetricsTable';
import GlobalMetrics from './GlobalMetrics';
import MetricsChart from './MetricsChart';
type Tab = 'table' | 'global' | 'chart';
type Tab = 'table' | 'global' | 'charts';
const TAB_CONFIG: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'table', label: 'Por Processo', icon: <Table size={13} /> },
{ id: 'table', label: 'Per Process', icon: <Table size={13} /> },
{ id: 'global', label: 'Global', icon: <Globe size={13} /> },
{ id: 'chart', label: 'Gráfico', icon: <BarChart2 size={13} /> },
{ id: 'charts', label: 'Charts', icon: <BarChart2 size={13} /> },
];
const MetricsPanel = () => {
@@ -31,7 +31,7 @@ const MetricsPanel = () => {
if (!hasData) {
return (
<div className="border-t border-slate-700 bg-slate-950 flex items-center justify-center h-32 text-slate-600 text-sm shrink-0">
Execute a simulação para ver as métricas.
Run the simulation to see metrics.
</div>
);
}
@@ -46,7 +46,7 @@ const MetricsPanel = () => {
const canExportCSV = status === 'finished' && processMetrics.length > 0;
return (
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '340px' }}>
<div className="border-t border-slate-700 bg-slate-950 flex flex-col shrink-0" style={{ height: '600px' }}>
{/* Panel header with tabs + export button */}
<div className="flex items-center px-3 border-b border-slate-700 shrink-0 h-10 gap-1">
{TAB_CONFIG.map(tab => (
@@ -69,9 +69,9 @@ const MetricsPanel = () => {
<button
onClick={() => exportCSV(processMetrics, processMap)}
disabled={!canExportCSV}
title={!canExportCSV ? 'Disponível ao concluir a simulação' : 'Exportar métricas como CSV'}
title={!canExportCSV ? 'Available when simulation completes' : 'Export metrics as CSV'}
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 px-2.5 py-1 rounded-md hover:bg-slate-800 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
aria-label="Exportar métricas CSV"
aria-label="Export metrics as CSV"
>
<Download size={12} />
CSV
@@ -87,7 +87,7 @@ const MetricsPanel = () => {
{activeTab === 'global' && (
<GlobalMetrics metrics={globalMetrics} />
)}
{activeTab === 'chart' && (
{activeTab === 'charts' && (
<MetricsChart metrics={processMetrics} processMap={processMap} />
)}
</div>
+4 -4
View File
@@ -22,7 +22,7 @@ interface MiniBarProps {
const MiniBar = ({ value, max, color }: MiniBarProps) => {
const pct = max > 0 ? (value / max) * 100 : 0;
return (
<div className="relative flex items-center justify-end gap-1.5 min-w-[52px]">
<div className="relative flex items-center justify-end gap-1.5 min-w-13">
<div
className="absolute inset-y-0 right-0 rounded-sm opacity-20 transition-all duration-300"
style={{ width: `${pct}%`, backgroundColor: color }}
@@ -36,7 +36,7 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
if (metrics.length === 0) {
return (
<div className="flex items-center justify-center h-32 text-slate-500 text-sm">
Nenhum processo concluído ainda.
No completed processes yet.
</div>
);
}
@@ -50,10 +50,10 @@ const MetricsTable = ({ metrics, processMap }: MetricsTableProps) => {
return (
<div className="overflow-x-auto">
<table className="w-full text-xs text-slate-300 border-collapse min-w-[780px]">
<table className="w-full text-xs text-slate-300 border-collapse min-w-195">
<thead>
<tr className="text-[10px] text-slate-400 uppercase tracking-wider border-b border-slate-700 sticky top-0 bg-slate-950 z-10">
<th className="text-left px-3 py-2 font-medium">Processo</th>
<th className="text-left px-3 py-2 font-medium">Process</th>
<th className="text-right px-3 py-2 font-medium">Arrival</th>
<th className="text-right px-3 py-2 font-medium">Start</th>
<th className="text-right px-3 py-2 font-medium">Finish</th>
+2 -2
View File
@@ -56,7 +56,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
<IconButton
size="sm"
variant="ghost"
label={`Editar ${process.name}`}
label={`Edit ${process.name}`}
onClick={() => onEdit(process)}
>
<Pencil size={13} />
@@ -64,7 +64,7 @@ const ProcessCard = ({ process, onEdit, onRemove }: ProcessCardProps) => {
<IconButton
size="sm"
variant="ghost"
label={`Remover ${process.name}`}
label={`Remove ${process.name}`}
onClick={() => onRemove(process.id)}
className="hover:text-red-400 hover:bg-red-900/20"
>
+24 -24
View File
@@ -69,16 +69,16 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
const validate = (): boolean => {
const errs: string[] = [];
if (!name.trim()) errs.push('Nome é obrigatório');
if (name.trim().length > 8) errs.push('Nome deve ter no máximo 8 caracteres');
if (arrivalTime < 0) errs.push('Arrival Time deve ser ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority deve estar entre 1 e 10');
if (bursts.length === 0) errs.push('Deve haver ao menos 1 burst CPU');
if (bursts[0]?.type !== 'cpu') errs.push('O primeiro burst deve ser CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('O último burst deve ser CPU');
if (!name.trim()) errs.push('Name is required');
if (name.trim().length > 8) errs.push('Name must be at most 8 characters');
if (arrivalTime < 0) errs.push('Arrival Time must be ≥ 0');
if (priority < 1 || priority > 10) errs.push('Priority must be between 1 and 10');
if (bursts.length === 0) errs.push('There must be at least 1 CPU burst');
if (bursts[0]?.type !== 'cpu') errs.push('The first burst must be CPU');
if (bursts.at(-1)?.type !== 'cpu') errs.push('The last burst must be CPU');
bursts.forEach((b, i) => {
if (b.duration < 1) errs.push(`Burst ${i + 1}: duração deve ser ≥ 1`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} e ${i + 1} não podem ser do mesmo tipo (alternar CPU/IO)`);
if (b.duration < 1) errs.push(`Burst ${i + 1}: duration must be ≥ 1`);
if (i > 0 && bursts[i - 1]?.type === b.type) errs.push(`Bursts ${i} and ${i + 1} cannot be the same type (alternate CPU/IO)`);
});
setErrors(errs);
return errs.length === 0;
@@ -108,9 +108,9 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div className="bg-slate-800/60 rounded-xl border border-slate-600 shadow-xl p-4 space-y-4" data-testid="process-form">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-200">
{isEditing ? 'Editar Processo' : 'Novo Processo'}
{isEditing ? 'Edit Process' : 'New Process'}
</h3>
<IconButton size="sm" variant="ghost" label="Fechar" onClick={onClose}>
<IconButton size="sm" variant="ghost" label="Close" onClick={onClose}>
<X size={14} />
</IconButton>
</div>
@@ -120,7 +120,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div className="col-span-2 flex gap-3">
{/* Name */}
<div className="flex-1">
<label className={labelCls} htmlFor="pf-name">Nome</label>
<label className={labelCls} htmlFor="pf-name">Name</label>
<input
id="pf-name"
type="text"
@@ -129,19 +129,19 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
onChange={e => setName(e.target.value)}
placeholder="P1"
className={inputCls}
aria-label="Nome do processo"
aria-label="Process name"
/>
</div>
{/* Color */}
<div>
<label className={labelCls} htmlFor="pf-color">Cor</label>
<label className={labelCls} htmlFor="pf-color">Color</label>
<input
id="pf-color"
type="color"
value={color}
onChange={e => setColor(e.target.value)}
className="h-8.5 w-12 rounded-md border border-slate-600 bg-slate-700 cursor-pointer"
aria-label="Cor do processo"
aria-label="Process color"
/>
</div>
</div>
@@ -156,7 +156,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={arrivalTime}
onChange={e => setArrivalTime(Number(e.target.value))}
className={inputCls}
aria-label="Tempo de chegada"
aria-label="Arrival time"
/>
</div>
@@ -171,7 +171,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={priority}
onChange={e => setPriority(Number(e.target.value))}
className={inputCls}
aria-label="Prioridade do processo"
aria-label="Process priority"
/>
</div>
</div>
@@ -180,7 +180,7 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">Bursts (CPU → I/O → CPU…)</label>
<IconButton size="sm" variant="ghost" label="Adicionar burst" onClick={addBurst}>
<IconButton size="sm" variant="ghost" label="Add burst" onClick={addBurst}>
<Plus size={13} />
</IconButton>
</div>
@@ -203,13 +203,13 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
value={burst.duration}
onChange={e => updateBurstDuration(idx, Number(e.target.value))}
className="w-full rounded bg-slate-700/60 border border-slate-600/50 text-slate-200 text-xs px-2 py-0.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Duração do burst ${idx + 1}`}
aria-label={`Burst ${idx + 1} duration`}
/>
<span className="text-[10px] text-slate-500">ticks</span>
<IconButton
size="sm"
variant="ghost"
label={`Remover burst ${idx + 1}`}
label={`Remove burst ${idx + 1}`}
onClick={() => removeBurst(idx)}
disabled={bursts.length <= 1}
className="hover:text-red-400"
@@ -235,17 +235,17 @@ const ProcessForm = ({ editingProcess, onClose }: ProcessFormProps) => {
<button
onClick={handleSubmit}
className="flex-1 flex items-center justify-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium py-1.5 rounded-lg transition-colors"
aria-label={isEditing ? 'Salvar processo' : 'Criar processo'}
aria-label={isEditing ? 'Save process' : 'Create process'}
>
<Check size={15} />
{isEditing ? 'Salvar' : 'Criar'}
{isEditing ? 'Save' : 'Create'}
</button>
<button
onClick={onClose}
className="px-4 bg-slate-700 hover:bg-slate-600 text-slate-300 text-sm rounded-lg transition-colors"
aria-label="Cancelar"
aria-label="Cancel"
>
Cancelar
Cancel
</button>
</div>
</div>
+7 -7
View File
@@ -47,13 +47,13 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
{/* List header */}
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">
{processes.length}/{MAX_PROCESSES} processos
{processes.length}/{MAX_PROCESSES} processes
</span>
<div className="flex gap-1">
<IconButton
size="sm"
variant="ghost"
label="Gerar processo aleatório"
label="Generate random process"
onClick={handleRandomize}
disabled={!canAdd}
>
@@ -62,7 +62,7 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
<IconButton
size="sm"
variant="ghost"
label="Limpar todos os processos"
label="Clear all processes"
onClick={clearProcesses}
disabled={processes.length === 0}
className="hover:text-red-400 hover:bg-red-900/20"
@@ -76,8 +76,8 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
<div className="space-y-2 max-h-64 overflow-y-auto pr-0.5 scrollbar-hide">
{processes.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 text-slate-500 text-sm py-8 border border-dashed border-slate-700 rounded-lg">
<span>Nenhum processo adicionado</span>
<span className="text-xs">Clique em + para criar</span>
<span>No processes added</span>
<span className="text-xs">Click + to create</span>
</div>
) : (
processes.map(p => (
@@ -95,11 +95,11 @@ const ProcessList = ({ onEditProcess, onAddNew }: ProcessListProps) => {
{canAdd && (
<button
onClick={onAddNew}
aria-label="Adicionar novo processo"
aria-label="Add new process"
className="mt-2 w-full flex items-center justify-center gap-2 py-2 rounded-lg border border-dashed border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400 text-xs transition-colors"
>
<Plus size={14} />
Adicionar Processo
Add Process
</button>
)}
</div>
+2 -2
View File
@@ -24,10 +24,10 @@ const ProcessPanel = () => {
};
return (
<section aria-label="Painel de Processos">
<section aria-label="Processes Panel">
<div className="flex items-center gap-2 mb-3 text-slate-300">
<ListTree size={18} className="text-blue-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Processos</h2>
<h2 className="font-semibold uppercase text-xs tracking-wider">Processes</h2>
</div>
{showForm ? (
@@ -6,9 +6,9 @@ const ALGORITHM_OPTIONS: { value: SchedulingAlgorithm; label: string; disabled?:
{ value: 'SJF_NP', label: 'SJF — Shortest Job First (NP)' },
{ value: 'SJF_P', label: 'SRTF — Shortest Remaining Time (P)' },
{ value: 'RR', label: 'RR — Round Robin' },
{ value: 'PRIORITY_NP',label: 'Priority (Não-preemptivo)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptivo)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (tempo real)' },
{ value: 'PRIORITY_NP',label: 'Priority (Non-preemptive)' },
{ value: 'PRIORITY_P', label: 'Priority (Preemptive)' },
{ value: 'PRIORITY_RR',label: 'Priority Round Robin (real-time)' },
{ value: 'HRRN', label: 'HRRN — Highest Response Ratio Next' },
{ value: 'MULTILEVEL', label: 'Multilevel Queue (MLQ)' },
];
@@ -23,14 +23,14 @@ const AlgorithmSelector = () => {
htmlFor="algorithm-select"
className="block text-xs text-slate-400 mb-1"
>
Algoritmo
Algorithm
</label>
<select
id="algorithm-select"
value={algorithm}
onChange={e => setConfig({ algorithm: e.target.value as SchedulingAlgorithm })}
className="w-full rounded-md bg-slate-700 border border-slate-600 text-slate-200 text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label="Selecionar algoritmo de escalonamento"
aria-label="Select scheduling algorithm"
>
{ALGORITHM_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
@@ -71,14 +71,14 @@ const MlqQueuesConfig = ({
return (
<div className="space-y-2 pt-1" data-testid="mlq-queues-config">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400 font-medium">Filas MLQ (prioridade decrescente)</span>
<span className="text-xs text-slate-400 font-medium">MLQ Queues (decreasing priority)</span>
<button
onClick={addQueue}
disabled={queues.length >= 5}
className="flex items-center gap-1 text-[10px] text-blue-400 hover:text-blue-300 disabled:text-slate-600 disabled:cursor-not-allowed transition-colors"
aria-label="Adicionar fila"
aria-label="Add queue"
>
<Plus size={11} /> Fila
<Plus size={11} /> Queue
</button>
</div>
{queues.map((q, idx) => (
@@ -89,46 +89,46 @@ const MlqQueuesConfig = ({
>
<div className="flex items-center justify-between">
<span className="text-[10px] text-slate-400 font-semibold uppercase tracking-wide">
Fila {idx} {idx === 0 ? '(mais alta)' : idx === queues.length - 1 ? '(mais baixa)' : ''}
Queue {idx} {idx === 0 ? '(highest)' : idx === queues.length - 1 ? '(lowest)' : ''}
</span>
<button
onClick={() => removeQueue(idx)}
disabled={queues.length <= 1}
className="text-slate-500 hover:text-red-400 disabled:text-slate-700 disabled:cursor-not-allowed transition-colors"
aria-label={`Remover fila ${idx}`}
aria-label={`Remove queue ${idx}`}
>
<Trash2 size={11} />
</button>
</div>
<div className="grid grid-cols-2 gap-1.5">
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. mín</label>
<label className="block text-[10px] text-slate-500 mb-0.5">Min. Prio</label>
<input
type="number" min={1} max={10}
value={q.priorityMin}
onChange={e => update(idx, { priorityMin: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade mínima`}
aria-label={`Queue ${idx}: minimum priority`}
/>
</div>
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Prio. máx</label>
<label className="block text-[10px] text-slate-500 mb-0.5">Max. Prio</label>
<input
type="number" min={1} max={10}
value={q.priorityMax}
onChange={e => update(idx, { priorityMax: Math.min(10, Math.max(1, Number(e.target.value))) })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500"
aria-label={`Fila ${idx}: prioridade máxima`}
aria-label={`Queue ${idx}: maximum priority`}
/>
</div>
</div>
<div>
<label className="block text-[10px] text-slate-500 mb-0.5">Algoritmo</label>
<label className="block text-[10px] text-slate-500 mb-0.5">Algorithm</label>
<select
value={q.algorithm}
onChange={e => update(idx, { algorithm: e.target.value as MlqQueueDef['algorithm'] })}
className="w-full rounded bg-slate-700 border border-slate-600 text-slate-200 text-xs px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
aria-label={`Fila ${idx}: algoritmo`}
aria-label={`Queue ${idx}: algorithm`}
>
{MLQ_ALGO_OPTIONS.map(a => (
<option key={a} value={a}>{a}</option>
@@ -167,7 +167,7 @@ const SchedulerConfig = () => {
{/* Read-only preemptive indicator */}
<div className="flex items-center justify-between">
<span className="text-xs text-slate-400">Tipo de escalonamento</span>
<span className="text-xs text-slate-400">Scheduling type</span>
<span
className={`text-[10px] font-semibold uppercase tracking-wide px-2 py-0.5 rounded border ${
isPreemptive
@@ -176,7 +176,7 @@ const SchedulerConfig = () => {
}`}
data-testid="preemptive-badge"
>
{isPreemptive ? 'Preemptivo' : 'Não-preemptivo'}
{isPreemptive ? 'Preemptive' : 'Non-preemptive'}
</span>
</div>
@@ -193,7 +193,7 @@ const SchedulerConfig = () => {
value={config.contextSwitchTime}
onChange={e => setConfig({ contextSwitchTime: Math.min(10, Math.max(0, Number(e.target.value))) })}
className={inputCls}
aria-label="Tempo de context switch"
aria-label="Context switch time"
/>
</div>
@@ -210,7 +210,7 @@ const SchedulerConfig = () => {
value={config.quantum}
onChange={e => setConfig({ quantum: Math.max(1, Number(e.target.value)) })}
className={inputCls}
aria-label="Quantum do Round Robin"
aria-label="Round Robin quantum"
/>
</div>
)}
@@ -227,7 +227,7 @@ const SchedulerConfig = () => {
{config.agingEnabled && (
<div data-testid="aging-interval-field">
<label htmlFor="aging-interval" className={labelCls}>
Intervalo de Aging (ticks)
Aging Interval (ticks)
</label>
<input
id="aging-interval"
@@ -236,7 +236,7 @@ const SchedulerConfig = () => {
value={config.agingInterval}
onChange={e => setConfig({ agingInterval: Math.max(1, Number(e.target.value)) })}
className={inputCls}
aria-label="Intervalo de aging"
aria-label="Aging interval"
/>
</div>
)}
@@ -4,10 +4,10 @@ import SchedulerConfig from './SchedulerConfig';
const SchedulerPanel = () => {
return (
<section aria-label="Painel do Escalonador" className="space-y-3">
<section aria-label="Scheduler Panel" className="space-y-3">
<div className="flex items-center gap-2 text-slate-300">
<Settings size={18} className="text-emerald-400" />
<h2 className="font-semibold uppercase text-xs tracking-wider">Escalonador</h2>
<h2 className="font-semibold uppercase text-xs tracking-wider">Scheduler</h2>
</div>
<AlgorithmSelector />
+2 -2
View File
@@ -38,9 +38,9 @@ describe('ControlBar', () => {
expect(screen.getByTestId('tick-display')).toHaveTextContent('0000');
});
it('shows "Pronto" status when idle', () => {
it('shows "Ready" status when idle', () => {
render(<ControlBar />);
expect(screen.getByText('Pronto')).toBeInTheDocument();
expect(screen.getByText('Ready')).toBeInTheDocument();
});
it('Play button is disabled when no processes are loaded', () => {
+22 -22
View File
@@ -11,62 +11,62 @@ beforeEach(() => {
});
describe('ProcessForm', () => {
it('renders the "Novo Processo" form with default fields', () => {
it('renders the "New Process" form with default fields', () => {
render(<ProcessForm onClose={() => {}} />);
expect(screen.getByTestId('process-form')).toBeInTheDocument();
expect(screen.getByText('Novo Processo')).toBeInTheDocument();
expect(screen.getByLabelText('Nome do processo')).toBeInTheDocument();
expect(screen.getByLabelText('Tempo de chegada')).toBeInTheDocument();
expect(screen.getByLabelText('Prioridade do processo')).toBeInTheDocument();
expect(screen.getByText('New Process')).toBeInTheDocument();
expect(screen.getByLabelText('Process name')).toBeInTheDocument();
expect(screen.getByLabelText('Arrival time')).toBeInTheDocument();
expect(screen.getByLabelText('Process priority')).toBeInTheDocument();
});
it('shows "Editar Processo" title when editingProcess is provided', () => {
it('shows "Edit Process" title when editingProcess is provided', () => {
const proc = {
id: 'p1', name: 'P1', arrivalTime: 0, priority: 5, color: '#fff',
bursts: [{ type: 'cpu' as const, duration: 4 }],
};
render(<ProcessForm editingProcess={proc} onClose={() => {}} />);
expect(screen.getByText('Editar Processo')).toBeInTheDocument();
expect(screen.getByText('Edit Process')).toBeInTheDocument();
});
it('shows validation error when name is empty', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
const nameInput = screen.getByLabelText('Process name');
await user.clear(nameInput);
const submitBtn = screen.getByRole('button', { name: /Criar/i });
const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn);
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(/Nome é obrigatório/i)).toBeInTheDocument();
expect(screen.getByText(/Name is required/i)).toBeInTheDocument();
});
it('shows validation error when arrival time is negative', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'P1');
const arrivalInput = screen.getByLabelText('Tempo de chegada');
const arrivalInput = screen.getByLabelText('Arrival time');
// fireEvent.change used to reliably set a negative number bypassing HTML min attribute
fireEvent.change(arrivalInput, { target: { value: '-1' } });
const submitBtn = screen.getByRole('button', { name: /Criar/i });
const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn);
expect(screen.getByText(/Arrival Time deve ser ≥ 0/i)).toBeInTheDocument();
expect(screen.getByText(/Arrival Time must be ≥ 0/i)).toBeInTheDocument();
});
it('shows validation error for name longer than 8 chars', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
const nameInput = screen.getByLabelText('Process name');
// Bypass the HTML maxLength attribute via fireEvent to test the JS validation
fireEvent.change(nameInput, { target: { value: 'ProcessXXLong' } });
await user.click(screen.getByRole('button', { name: /Criar/i }));
expect(screen.getByText(/Nome deve ter no máximo 8 caracteres/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /Create/i }));
expect(screen.getByText(/Name must be at most 8 characters/i)).toBeInTheDocument();
});
it('adds a new burst with alternating type when clicking + button', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const addBurstBtn = screen.getByLabelText('Adicionar burst');
const addBurstBtn = screen.getByLabelText('Add burst');
await user.click(addBurstBtn);
// After one CPU burst at start, clicking + adds IO burst
// getAllByText with exact matches for the type labels
@@ -80,9 +80,9 @@ describe('ProcessForm', () => {
const user = userEvent.setup();
const onClose = vi.fn();
render(<ProcessForm onClose={onClose} />);
const nameInput = screen.getByLabelText('Nome do processo');
const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'P1');
const submitBtn = screen.getByRole('button', { name: /Criar/i });
const submitBtn = screen.getByRole('button', { name: /Create/i });
await user.click(submitBtn);
expect(onClose).toHaveBeenCalledOnce();
});
@@ -90,9 +90,9 @@ describe('ProcessForm', () => {
it('adds the process to the store after successful submission', async () => {
const user = userEvent.setup();
render(<ProcessForm onClose={() => {}} />);
const nameInput = screen.getByLabelText('Nome do processo');
const nameInput = screen.getByLabelText('Process name');
await user.type(nameInput, 'Test1');
await user.click(screen.getByRole('button', { name: /Criar/i }));
await user.click(screen.getByRole('button', { name: /Create/i }));
const processes = useProcessStore.getState().processes;
expect(processes).toHaveLength(1);
expect(processes[0]?.name).toBe('Test1');
@@ -26,7 +26,7 @@ beforeEach(() => {
describe('SchedulerConfig', () => {
it('always shows Context Switch field regardless of algorithm', () => {
render(<SchedulerConfig />);
expect(screen.getByLabelText('Tempo de context switch')).toBeInTheDocument();
expect(screen.getByLabelText('Context switch time')).toBeInTheDocument();
});
it('always shows read-only preemptive badge', () => {
@@ -34,23 +34,23 @@ describe('SchedulerConfig', () => {
expect(screen.getByTestId('preemptive-badge')).toBeInTheDocument();
});
it('badge shows "Não-preemptivo" for FCFS', () => {
it('badge shows "Non-preemptive" for FCFS', () => {
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
});
it('badge shows "Preemptivo" for SJF_P (SRTF)', () => {
it('badge shows "Preemptive" for SJF_P (SRTF)', () => {
setAlgorithm('SJF_P');
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptivo');
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Preemptive');
});
it('badge shows "Não-preemptivo" for SJF_NP (preemptive toggle no longer exists)', () => {
it('badge shows "Non-preemptive" for SJF_NP (preemptive toggle no longer exists)', () => {
setAlgorithm('SJF_NP');
render(<SchedulerConfig />);
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Não-preemptivo');
expect(screen.getByTestId('preemptive-badge')).toHaveTextContent('Non-preemptive');
// Confirm the old interactive preemptive switch is gone
expect(screen.queryByRole('switch', { name: /Preemptivo/i })).not.toBeInTheDocument();
expect(screen.queryByRole('switch', { name: /Preemptive/i })).not.toBeInTheDocument();
});
it('does NOT show Quantum field for FCFS', () => {
@@ -62,7 +62,7 @@ describe('SchedulerConfig', () => {
setAlgorithm('RR');
render(<SchedulerConfig />);
expect(screen.getByTestId('quantum-field')).toBeInTheDocument();
expect(screen.getByLabelText('Quantum do Round Robin')).toBeInTheDocument();
expect(screen.getByLabelText('Round Robin quantum')).toBeInTheDocument();
});
it('shows MLQ queue config panel when algorithm is MULTILEVEL', () => {
@@ -97,13 +97,13 @@ describe('SchedulerConfig', () => {
const agingToggle = screen.getByRole('switch', { name: /Aging/i });
await user.click(agingToggle);
expect(screen.getByTestId('aging-interval-field')).toBeInTheDocument();
expect(screen.getByLabelText('Intervalo de aging')).toBeInTheDocument();
expect(screen.getByLabelText('Aging interval')).toBeInTheDocument();
});
it('updates the store when Quantum field is changed', () => {
setAlgorithm('RR');
render(<SchedulerConfig />);
const quantumInput = screen.getByLabelText('Quantum do Round Robin');
const quantumInput = screen.getByLabelText('Round Robin quantum');
fireEvent.change(quantumInput, { target: { value: '5' } });
expect(useProcessStore.getState().config.quantum).toBe(5);
});
+2 -2
View File
@@ -236,11 +236,11 @@ describe("SimulationEngine — ciclo de vida", () => {
});
// ============================================================
// SimulationEngine — chegada de processos
// SimulationEngine — process arrival
// Tests: late arrivals, idle ticks while waiting for process
// ============================================================
describe("SimulationEngine — chegada de processos", () => {
describe("SimulationEngine — process arrival", () => {
it("process arriving after simulation starts is correctly scheduled", () => {
const engine = new SimulationEngine(
[
+1 -1
View File
@@ -107,7 +107,7 @@ export const presets: Preset[] = [
},
{
name: "context_switch_demo",
description: "3 processos FCFS com 2 ticks de overhead — veja as células vermelhas de Context Switch",
description: "3 FCFS processes with 2 ticks of overhead — see the red Context Switch cells",
processes: [
{ pid: 1, id: "p1", name: "P1", arrivalTime: 0, priority: 5, color: "#4FC3F7", bursts: [{ type: "cpu", duration: 3 }] },
{ pid: 2, id: "p2", name: "P2", arrivalTime: 0, priority: 5, color: "#81C784", bursts: [{ type: "cpu", duration: 3 }] },
+9 -2
View File
@@ -18,7 +18,9 @@ function downloadBlob(content: string, filename: string, mimeType: string) {
export function useExport() {
const exportCSV = (metrics: ProcessMetrics[], processMap: ProcessMap) => {
const headers = [
"Processo",
"PID",
"Process",
"Priority",
"Arrival",
"Start",
"Finish",
@@ -32,9 +34,14 @@ export function useExport() {
];
const rows = metrics.map((m) => {
const name = processMap[m.processId]?.name ?? m.processId;
const proc = processMap[m.processId];
const pid = proc?.pid ?? "";
const name = proc?.name ?? m.processId;
const priority = proc?.priority ?? "";
return [
pid,
name,
priority,
m.arrivalTime,
m.startTick,
m.finishTick,