hyper-scheduler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +21 -0
- package/.eslintrc.cjs +26 -0
- package/GEMINI.md +1 -0
- package/README.md +38 -0
- package/docs/.vitepress/config.ts +52 -0
- package/docs/README.md +120 -0
- package/docs/api/devtools.md +232 -0
- package/docs/api/index.md +178 -0
- package/docs/api/scheduler.md +322 -0
- package/docs/api/task.md +439 -0
- package/docs/api/types.md +365 -0
- package/docs/examples/index.md +295 -0
- package/docs/guide/best-practices.md +436 -0
- package/docs/guide/core-concepts.md +363 -0
- package/docs/guide/getting-started.md +138 -0
- package/docs/index.md +33 -0
- package/docs/public/logo.svg +54 -0
- package/examples/browser/index.html +354 -0
- package/examples/node/simple.js +36 -0
- package/examples/react-demo/index.html +12 -0
- package/examples/react-demo/package.json +23 -0
- package/examples/react-demo/src/App.css +212 -0
- package/examples/react-demo/src/App.jsx +160 -0
- package/examples/react-demo/src/main.jsx +9 -0
- package/examples/react-demo/vite.config.ts +12 -0
- package/examples/react-demo/yarn.lock +752 -0
- package/examples/vue-demo/index.html +12 -0
- package/examples/vue-demo/package.json +21 -0
- package/examples/vue-demo/src/App.vue +373 -0
- package/examples/vue-demo/src/main.ts +4 -0
- package/examples/vue-demo/vite.config.ts +13 -0
- package/examples/vue-demo/yarn.lock +375 -0
- package/package.json +51 -0
- package/src/constants.ts +18 -0
- package/src/core/retry-strategy.ts +28 -0
- package/src/core/scheduler.ts +601 -0
- package/src/core/task-registry.ts +58 -0
- package/src/index.ts +74 -0
- package/src/platform/browser/browser-timer.ts +66 -0
- package/src/platform/browser/main-thread-timer.ts +16 -0
- package/src/platform/browser/worker.ts +31 -0
- package/src/platform/node/debug-cli.ts +19 -0
- package/src/platform/node/node-timer.ts +15 -0
- package/src/platform/timer-strategy.ts +19 -0
- package/src/plugins/dev-tools.ts +101 -0
- package/src/types.ts +115 -0
- package/src/ui/components/devtools.ts +525 -0
- package/src/ui/components/floating-trigger.ts +102 -0
- package/src/ui/components/icons.ts +16 -0
- package/src/ui/components/resizer.ts +129 -0
- package/src/ui/components/task-detail.ts +228 -0
- package/src/ui/components/task-header.ts +319 -0
- package/src/ui/components/task-list.ts +416 -0
- package/src/ui/components/timeline.ts +364 -0
- package/src/ui/debug-panel.ts +56 -0
- package/src/ui/i18n/en.ts +76 -0
- package/src/ui/i18n/index.ts +42 -0
- package/src/ui/i18n/zh.ts +76 -0
- package/src/ui/store/dev-tools-store.ts +191 -0
- package/src/ui/styles/theme.css.ts +56 -0
- package/src/ui/styles.ts +43 -0
- package/src/utils/cron-lite.ts +221 -0
- package/src/utils/cron.ts +20 -0
- package/src/utils/id.ts +10 -0
- package/src/utils/schedule.ts +93 -0
- package/src/vite-env.d.ts +1 -0
- package/stats.html +4949 -0
- package/tests/integration/Debug.test.ts +58 -0
- package/tests/unit/Plugin.test.ts +16 -0
- package/tests/unit/RetryStrategy.test.ts +21 -0
- package/tests/unit/Scheduler.test.ts +38 -0
- package/tests/unit/schedule.test.ts +70 -0
- package/tests/unit/ui/DevToolsStore.test.ts +67 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +51 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { themeStyles } from '../styles/theme.css';
|
|
2
|
+
import { TaskSnapshot, ExecutionRecord } from '../../types';
|
|
3
|
+
import { t } from '../i18n';
|
|
4
|
+
|
|
5
|
+
export class Timeline extends HTMLElement {
|
|
6
|
+
private _shadow: ShadowRoot;
|
|
7
|
+
private _tasks: Map<string, TaskSnapshot> = new Map();
|
|
8
|
+
private _history: Map<string, ExecutionRecord[]> = new Map();
|
|
9
|
+
|
|
10
|
+
private $canvas!: HTMLCanvasElement;
|
|
11
|
+
private ctx!: CanvasRenderingContext2D;
|
|
12
|
+
private timeRange = 60 * 1000; // 1 minute window
|
|
13
|
+
private zoom = 1;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this._shadow = this.attachShadow({ mode: 'open' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connectedCallback() {
|
|
21
|
+
this.render();
|
|
22
|
+
this.$canvas = this._shadow.querySelector('canvas')!;
|
|
23
|
+
if (!this.$canvas) {
|
|
24
|
+
console.error('[Timeline] Canvas not found');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.ctx = this.$canvas.getContext('2d')!;
|
|
29
|
+
if (!this.ctx) {
|
|
30
|
+
console.error('[Timeline] Canvas context not available');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.setupZoom();
|
|
35
|
+
this.startLoop();
|
|
36
|
+
|
|
37
|
+
// Add ResizeObserver to canvas container
|
|
38
|
+
const container = this._shadow.querySelector('.canvas-container');
|
|
39
|
+
if (container) {
|
|
40
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
41
|
+
// Trigger a redraw on next frame
|
|
42
|
+
requestAnimationFrame(() => this.draw());
|
|
43
|
+
});
|
|
44
|
+
resizeObserver.observe(container);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set data(val: { tasks: Map<string, TaskSnapshot>, history: Map<string, ExecutionRecord[]> }) {
|
|
49
|
+
this._tasks = val.tasks;
|
|
50
|
+
this._history = val.history;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
set defaultZoom(val: number) {
|
|
54
|
+
if (val >= 0.5 && val <= 5) {
|
|
55
|
+
this.zoom = val;
|
|
56
|
+
this.timeRange = 60 * 1000 / this.zoom;
|
|
57
|
+
// 更新 slider 和 label
|
|
58
|
+
const zoomSlider = this._shadow.querySelector('.zoom-slider') as HTMLInputElement;
|
|
59
|
+
if (zoomSlider) zoomSlider.value = val.toString();
|
|
60
|
+
this.updateZoomLabel();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Method to update texts when language changes
|
|
65
|
+
updateTexts() {
|
|
66
|
+
this.updateZoomLabel();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private updateZoomLabel() {
|
|
70
|
+
const zoomLabel = this._shadow.querySelector('.zoom-label');
|
|
71
|
+
if (zoomLabel) {
|
|
72
|
+
zoomLabel.textContent = `${t('timeline.zoom')}: ${this.zoom}x (${Math.round(this.timeRange / 1000)}s)`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private setupZoom() {
|
|
77
|
+
const zoomSlider = this._shadow.querySelector('.zoom-slider') as HTMLInputElement;
|
|
78
|
+
const zoomOut = this._shadow.querySelector('.zoom-out');
|
|
79
|
+
const zoomIn = this._shadow.querySelector('.zoom-in');
|
|
80
|
+
|
|
81
|
+
const updateZoom = (newZoom: number) => {
|
|
82
|
+
this.zoom = newZoom;
|
|
83
|
+
this.timeRange = 60 * 1000 / this.zoom;
|
|
84
|
+
this.updateZoomLabel();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
zoomSlider?.addEventListener('input', (e) => {
|
|
88
|
+
const newZoom = parseFloat((e.target as HTMLInputElement).value);
|
|
89
|
+
updateZoom(newZoom);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
zoomOut?.addEventListener('click', () => {
|
|
93
|
+
const newZoom = Math.max(0.5, this.zoom - 0.5);
|
|
94
|
+
if (zoomSlider) zoomSlider.value = newZoom.toString();
|
|
95
|
+
updateZoom(newZoom);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
zoomIn?.addEventListener('click', () => {
|
|
99
|
+
const newZoom = Math.min(5, this.zoom + 0.5);
|
|
100
|
+
if (zoomSlider) zoomSlider.value = newZoom.toString();
|
|
101
|
+
updateZoom(newZoom);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Initial label update
|
|
105
|
+
this.updateZoomLabel();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private startLoop() {
|
|
109
|
+
const loop = () => {
|
|
110
|
+
if (this.isConnected) {
|
|
111
|
+
this.draw();
|
|
112
|
+
requestAnimationFrame(loop);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
requestAnimationFrame(loop);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private draw() {
|
|
119
|
+
if (!this.ctx || !this.$canvas) return;
|
|
120
|
+
|
|
121
|
+
// Get container size
|
|
122
|
+
const container = this._shadow.querySelector('.canvas-container') as HTMLElement;
|
|
123
|
+
if (!container) return;
|
|
124
|
+
|
|
125
|
+
const dpr = window.devicePixelRatio || 1;
|
|
126
|
+
const width = container.clientWidth;
|
|
127
|
+
|
|
128
|
+
if (width === 0) return;
|
|
129
|
+
|
|
130
|
+
// Calculate required height based on number of tasks
|
|
131
|
+
const rowHeight = 40;
|
|
132
|
+
const taskIds = Array.from(this._tasks.keys());
|
|
133
|
+
const headerHeight = 60;
|
|
134
|
+
const footerHeight = 40;
|
|
135
|
+
const minHeight = container.clientHeight || 300;
|
|
136
|
+
const contentHeight = taskIds.length * rowHeight + headerHeight + footerHeight;
|
|
137
|
+
const height = Math.max(minHeight, contentHeight);
|
|
138
|
+
|
|
139
|
+
// Set canvas size - this resets the transform
|
|
140
|
+
this.$canvas.width = width * dpr;
|
|
141
|
+
this.$canvas.height = height * dpr;
|
|
142
|
+
this.$canvas.style.width = `${width}px`;
|
|
143
|
+
this.$canvas.style.height = `${height}px`;
|
|
144
|
+
|
|
145
|
+
// Reset and apply scale (must be after setting canvas size)
|
|
146
|
+
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
147
|
+
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
const startTime = now - this.timeRange;
|
|
150
|
+
const labelWidth = 150;
|
|
151
|
+
|
|
152
|
+
// Get computed colors from host element
|
|
153
|
+
const hostStyles = getComputedStyle(this);
|
|
154
|
+
const bgColor = hostStyles.getPropertyValue('--hs-bg').trim() || '#1e1e1e';
|
|
155
|
+
const textColor = hostStyles.getPropertyValue('--hs-text').trim() || '#fff';
|
|
156
|
+
const textSecondary = hostStyles.getPropertyValue('--hs-text-secondary').trim() || '#888';
|
|
157
|
+
const borderColor = hostStyles.getPropertyValue('--hs-border').trim() || '#333';
|
|
158
|
+
const successColor = hostStyles.getPropertyValue('--hs-success').trim() || '#22c55e';
|
|
159
|
+
const dangerColor = hostStyles.getPropertyValue('--hs-danger').trim() || '#ef4444';
|
|
160
|
+
|
|
161
|
+
// Clear
|
|
162
|
+
this.ctx.fillStyle = bgColor;
|
|
163
|
+
this.ctx.fillRect(0, 0, width, height);
|
|
164
|
+
|
|
165
|
+
// Draw time axis
|
|
166
|
+
this.drawTimeAxis(width, labelWidth, startTime, textSecondary, borderColor);
|
|
167
|
+
|
|
168
|
+
// Draw task rows
|
|
169
|
+
taskIds.forEach((taskId, index) => {
|
|
170
|
+
const y = index * rowHeight + 60;
|
|
171
|
+
this.drawTaskRow(taskId, y, width, labelWidth, startTime, now, textColor, textSecondary, borderColor, successColor, dangerColor);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Draw legend
|
|
175
|
+
this.drawLegend(width, height, textSecondary, successColor);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private drawTimeAxis(width: number, labelWidth: number, startTime: number, textSecondary: string, borderColor: string) {
|
|
179
|
+
const ctx = this.ctx;
|
|
180
|
+
const timelineWidth = width - labelWidth - 20;
|
|
181
|
+
const segments = 4;
|
|
182
|
+
|
|
183
|
+
ctx.fillStyle = textSecondary;
|
|
184
|
+
ctx.font = '10px monospace';
|
|
185
|
+
ctx.textAlign = 'center';
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i <= segments; i++) {
|
|
188
|
+
const x = labelWidth + (timelineWidth / segments) * i;
|
|
189
|
+
const time = startTime + (this.timeRange / segments) * i;
|
|
190
|
+
const timeStr = new Date(time).toLocaleTimeString('en-US', {
|
|
191
|
+
hour: '2-digit',
|
|
192
|
+
minute: '2-digit',
|
|
193
|
+
second: '2-digit',
|
|
194
|
+
hour12: false
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
ctx.fillText(timeStr, x, 30);
|
|
198
|
+
|
|
199
|
+
// Draw vertical grid line
|
|
200
|
+
ctx.strokeStyle = borderColor;
|
|
201
|
+
ctx.beginPath();
|
|
202
|
+
ctx.moveTo(x, 40);
|
|
203
|
+
ctx.lineTo(x, this.$canvas.clientHeight - 40);
|
|
204
|
+
ctx.stroke();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Draw time range label
|
|
208
|
+
ctx.textAlign = 'left';
|
|
209
|
+
ctx.fillText(t('timeline.timeRange', { n: Math.round(this.timeRange / 1000) }), 10, 15);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private drawTaskRow(taskId: string, y: number, width: number, labelWidth: number, startTime: number, endTime: number, textColor: string, _textSecondary: string, borderColor: string, successColor: string, dangerColor: string) {
|
|
213
|
+
const ctx = this.ctx;
|
|
214
|
+
const timelineWidth = width - labelWidth - 20;
|
|
215
|
+
|
|
216
|
+
// Draw task label
|
|
217
|
+
ctx.fillStyle = textColor;
|
|
218
|
+
ctx.font = '11px sans-serif';
|
|
219
|
+
ctx.textAlign = 'left';
|
|
220
|
+
ctx.fillText(taskId, 10, y + 15);
|
|
221
|
+
|
|
222
|
+
// Draw driver indicator - get from task snapshot
|
|
223
|
+
const task = this._tasks.get(taskId);
|
|
224
|
+
const driver = (task as any)?.driver === 'main' ? 'M' : 'W';
|
|
225
|
+
const driverColor = driver === 'W' ? '#22c55e' : '#f59e0b';
|
|
226
|
+
ctx.fillStyle = driverColor;
|
|
227
|
+
ctx.font = '9px monospace';
|
|
228
|
+
ctx.fillText(`[${driver}]`, 10, y + 28);
|
|
229
|
+
|
|
230
|
+
// Draw separator line
|
|
231
|
+
ctx.strokeStyle = borderColor;
|
|
232
|
+
ctx.beginPath();
|
|
233
|
+
ctx.moveTo(0, y + 35);
|
|
234
|
+
ctx.lineTo(width, y + 35);
|
|
235
|
+
ctx.stroke();
|
|
236
|
+
|
|
237
|
+
// Draw execution history
|
|
238
|
+
const logs = this._history.get(taskId) || [];
|
|
239
|
+
logs.forEach(log => {
|
|
240
|
+
if (log.timestamp < startTime || log.timestamp > endTime) return;
|
|
241
|
+
|
|
242
|
+
const x = labelWidth + ((log.timestamp - startTime) / this.timeRange) * timelineWidth;
|
|
243
|
+
const duration = log.duration;
|
|
244
|
+
|
|
245
|
+
if (duration < 10) {
|
|
246
|
+
// Draw as dot for instant tasks
|
|
247
|
+
ctx.fillStyle = log.success ? successColor : dangerColor;
|
|
248
|
+
ctx.beginPath();
|
|
249
|
+
ctx.arc(x, y + 15, 3, 0, Math.PI * 2);
|
|
250
|
+
ctx.fill();
|
|
251
|
+
} else {
|
|
252
|
+
// Draw as bar for long tasks
|
|
253
|
+
const barWidth = Math.max(2, (duration / this.timeRange) * timelineWidth);
|
|
254
|
+
ctx.fillStyle = log.success ? successColor : dangerColor;
|
|
255
|
+
ctx.globalAlpha = 0.7;
|
|
256
|
+
ctx.fillRect(x, y + 5, barWidth, 20);
|
|
257
|
+
ctx.globalAlpha = 1;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private drawLegend(_width: number, height: number, textSecondary: string, successColor: string) {
|
|
263
|
+
const ctx = this.ctx;
|
|
264
|
+
const legendY = height - 25;
|
|
265
|
+
|
|
266
|
+
ctx.font = '10px sans-serif';
|
|
267
|
+
ctx.textAlign = 'left';
|
|
268
|
+
ctx.fillStyle = textSecondary;
|
|
269
|
+
|
|
270
|
+
let x = 10;
|
|
271
|
+
|
|
272
|
+
// Dot legend
|
|
273
|
+
ctx.fillText(`${t('timeline.legend')}:`, x, legendY);
|
|
274
|
+
x += 50;
|
|
275
|
+
|
|
276
|
+
ctx.fillStyle = successColor;
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
ctx.arc(x, legendY - 4, 3, 0, Math.PI * 2);
|
|
279
|
+
ctx.fill();
|
|
280
|
+
ctx.fillStyle = textSecondary;
|
|
281
|
+
ctx.fillText(t('timeline.instant'), x + 10, legendY);
|
|
282
|
+
x += 60;
|
|
283
|
+
|
|
284
|
+
// Bar legend
|
|
285
|
+
ctx.fillStyle = successColor;
|
|
286
|
+
ctx.globalAlpha = 0.7;
|
|
287
|
+
ctx.fillRect(x, legendY - 8, 15, 10);
|
|
288
|
+
ctx.globalAlpha = 1;
|
|
289
|
+
ctx.fillStyle = textSecondary;
|
|
290
|
+
ctx.fillText(t('timeline.duration'), x + 20, legendY);
|
|
291
|
+
x += 80;
|
|
292
|
+
|
|
293
|
+
// Driver legend
|
|
294
|
+
ctx.fillText(`[W] ${t('timeline.workerDriver')}`, x, legendY);
|
|
295
|
+
x += 110;
|
|
296
|
+
ctx.fillText(`[M] ${t('timeline.mainDriver')}`, x, legendY);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
render() {
|
|
300
|
+
this._shadow.innerHTML = `
|
|
301
|
+
<style>
|
|
302
|
+
${themeStyles}
|
|
303
|
+
:host {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
height: 100%;
|
|
307
|
+
background: var(--hs-bg);
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
}
|
|
310
|
+
.controls {
|
|
311
|
+
display: flex;
|
|
312
|
+
justify-content: flex-end;
|
|
313
|
+
align-items: center;
|
|
314
|
+
padding: 12px 16px;
|
|
315
|
+
gap: 8px;
|
|
316
|
+
border-bottom: 1px solid var(--hs-border);
|
|
317
|
+
flex-shrink: 0;
|
|
318
|
+
}
|
|
319
|
+
.zoom-label {
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
color: var(--hs-text-secondary);
|
|
322
|
+
}
|
|
323
|
+
.zoom-btn {
|
|
324
|
+
background: var(--hs-bg-secondary);
|
|
325
|
+
border: 1px solid var(--hs-border);
|
|
326
|
+
color: var(--hs-text);
|
|
327
|
+
width: 24px;
|
|
328
|
+
height: 24px;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
display: flex;
|
|
332
|
+
align-items: center;
|
|
333
|
+
justify-content: center;
|
|
334
|
+
}
|
|
335
|
+
.zoom-btn:hover {
|
|
336
|
+
background: var(--hs-primary);
|
|
337
|
+
color: white;
|
|
338
|
+
}
|
|
339
|
+
.zoom-slider {
|
|
340
|
+
width: 100px;
|
|
341
|
+
}
|
|
342
|
+
.canvas-container {
|
|
343
|
+
flex: 1;
|
|
344
|
+
overflow: auto;
|
|
345
|
+
position: relative;
|
|
346
|
+
}
|
|
347
|
+
canvas {
|
|
348
|
+
display: block;
|
|
349
|
+
}
|
|
350
|
+
</style>
|
|
351
|
+
<div class="controls">
|
|
352
|
+
<span class="zoom-label">Zoom:</span>
|
|
353
|
+
<button class="zoom-btn zoom-out">-</button>
|
|
354
|
+
<input type="range" class="zoom-slider" min="0.5" max="5" step="0.5" value="1">
|
|
355
|
+
<button class="zoom-btn zoom-in">+</button>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="canvas-container">
|
|
358
|
+
<canvas></canvas>
|
|
359
|
+
</div>
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
customElements.define('hs-timeline', Timeline);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Task } from '../types';
|
|
2
|
+
import { styles } from './styles';
|
|
3
|
+
|
|
4
|
+
export class DebugPanel {
|
|
5
|
+
private container: HTMLElement | null = null;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
if (typeof document !== 'undefined') {
|
|
9
|
+
this.init();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private init(): void {
|
|
14
|
+
const styleEl = document.createElement('style');
|
|
15
|
+
styleEl.textContent = styles;
|
|
16
|
+
document.head.appendChild(styleEl);
|
|
17
|
+
|
|
18
|
+
this.container = document.createElement('div');
|
|
19
|
+
this.container.id = 'hyper-scheduler-debug-panel';
|
|
20
|
+
this.container.style.display = 'none'; // Hidden by default
|
|
21
|
+
document.body.appendChild(this.container);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
show(): void {
|
|
25
|
+
if (this.container) this.container.style.display = 'block';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
hide(): void {
|
|
29
|
+
if (this.container) this.container.style.display = 'none';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
update(tasks: Task[]): void {
|
|
33
|
+
if (!this.container) return;
|
|
34
|
+
|
|
35
|
+
let html = '<div class="hs-header"><span>Hyper Scheduler</span><button id="hs-close-btn" style="float:right">x</button></div>';
|
|
36
|
+
|
|
37
|
+
tasks.forEach(task => {
|
|
38
|
+
const nextRun = task.nextRun ? new Date(task.nextRun).toLocaleTimeString() : '-';
|
|
39
|
+
html += `
|
|
40
|
+
<div class="hs-task-row">
|
|
41
|
+
<span>${task.id}</span>
|
|
42
|
+
<span class="hs-status-${task.status}">${task.status}</span>
|
|
43
|
+
<span>Next: ${nextRun}</span>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.container.innerHTML = html;
|
|
49
|
+
|
|
50
|
+
// Re-bind close button
|
|
51
|
+
const closeBtn = this.container.querySelector('#hs-close-btn');
|
|
52
|
+
if (closeBtn) {
|
|
53
|
+
closeBtn.addEventListener('click', () => this.hide());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
header: {
|
|
3
|
+
title: 'DevTools',
|
|
4
|
+
searchPlaceholder: 'Search IDs/Tags... 🔍',
|
|
5
|
+
toggleDock: 'Toggle Dock Position',
|
|
6
|
+
toggleTheme: 'Toggle Theme',
|
|
7
|
+
close: 'Close'
|
|
8
|
+
},
|
|
9
|
+
stats: {
|
|
10
|
+
loading: 'Loading...',
|
|
11
|
+
fps: 'FPS',
|
|
12
|
+
status: 'Status',
|
|
13
|
+
active: 'Active',
|
|
14
|
+
total: 'Total',
|
|
15
|
+
mainThread: 'Main Thread',
|
|
16
|
+
scheduler: 'Scheduler',
|
|
17
|
+
running: 'Running',
|
|
18
|
+
stopped: 'Stopped'
|
|
19
|
+
},
|
|
20
|
+
tabs: {
|
|
21
|
+
tasks: 'Tasks List',
|
|
22
|
+
timeline: 'Timeline'
|
|
23
|
+
},
|
|
24
|
+
list: {
|
|
25
|
+
idTags: 'ID / Tags',
|
|
26
|
+
status: 'Status',
|
|
27
|
+
driver: 'Driver',
|
|
28
|
+
driverWorker: 'Worker (Web Worker)',
|
|
29
|
+
driverMain: 'Main (setTimeout)',
|
|
30
|
+
schedule: 'Schedule',
|
|
31
|
+
count: 'Count',
|
|
32
|
+
lastRun: 'Last Run',
|
|
33
|
+
actions: 'Actions',
|
|
34
|
+
noTags: '(No Tags)',
|
|
35
|
+
tip: '✨ Tip: Click a row for details & history.'
|
|
36
|
+
},
|
|
37
|
+
detail: {
|
|
38
|
+
back: 'Back',
|
|
39
|
+
details: 'Task Details',
|
|
40
|
+
config: 'Config',
|
|
41
|
+
history: 'Execution History',
|
|
42
|
+
lastRuns: 'Last {n} runs',
|
|
43
|
+
avgDuration: 'Avg Duration',
|
|
44
|
+
startTime: 'Start Time',
|
|
45
|
+
duration: 'Duration',
|
|
46
|
+
drift: 'Drift',
|
|
47
|
+
status: 'Status',
|
|
48
|
+
noHistory: 'No execution history',
|
|
49
|
+
noTask: 'No task selected',
|
|
50
|
+
success: 'Success',
|
|
51
|
+
failed: 'Failed',
|
|
52
|
+
error: 'Error'
|
|
53
|
+
},
|
|
54
|
+
timeline: {
|
|
55
|
+
zoom: 'Zoom',
|
|
56
|
+
timeRange: 'Time Range: Last {n}s',
|
|
57
|
+
legend: 'Legend',
|
|
58
|
+
instant: 'Instant',
|
|
59
|
+
duration: 'Duration',
|
|
60
|
+
workerDriver: 'Worker Driver',
|
|
61
|
+
mainDriver: 'Main Driver'
|
|
62
|
+
},
|
|
63
|
+
status: {
|
|
64
|
+
running: 'Running',
|
|
65
|
+
paused: 'Paused',
|
|
66
|
+
stopped: 'Stopped',
|
|
67
|
+
idle: 'Scheduled',
|
|
68
|
+
error: 'Error'
|
|
69
|
+
},
|
|
70
|
+
actions: {
|
|
71
|
+
trigger: 'Trigger now',
|
|
72
|
+
start: 'Start task',
|
|
73
|
+
stop: 'Stop task',
|
|
74
|
+
remove: 'Remove task'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { en } from './en';
|
|
2
|
+
import { zh } from './zh';
|
|
3
|
+
|
|
4
|
+
type Lang = 'en' | 'zh';
|
|
5
|
+
type Translation = typeof en;
|
|
6
|
+
|
|
7
|
+
let currentLang: Lang = 'en';
|
|
8
|
+
let translations: Translation = en;
|
|
9
|
+
|
|
10
|
+
export function setLanguage(lang: Lang) {
|
|
11
|
+
currentLang = lang;
|
|
12
|
+
translations = lang === 'zh' ? zh : en;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getLanguage(): Lang {
|
|
16
|
+
return currentLang;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function t(key: string, params?: Record<string, any>): string {
|
|
20
|
+
const keys = key.split('.');
|
|
21
|
+
let value: any = translations;
|
|
22
|
+
|
|
23
|
+
for (const k of keys) {
|
|
24
|
+
if (value && typeof value === 'object' && k in value) {
|
|
25
|
+
value = value[k as keyof typeof value];
|
|
26
|
+
} else {
|
|
27
|
+
return key; // Fallback to key if not found
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof value !== 'string') {
|
|
32
|
+
return key;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (params) {
|
|
36
|
+
return value.replace(/\{(\w+)\}/g, (_, k) => {
|
|
37
|
+
return params[k] !== undefined ? String(params[k]) : `{${k}}`;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
header: {
|
|
3
|
+
title: '调试工具',
|
|
4
|
+
searchPlaceholder: '搜索 ID/标签... 🔍',
|
|
5
|
+
toggleDock: '切换停靠位置',
|
|
6
|
+
toggleTheme: '切换主题',
|
|
7
|
+
close: '关闭'
|
|
8
|
+
},
|
|
9
|
+
stats: {
|
|
10
|
+
loading: '加载中...',
|
|
11
|
+
fps: '帧率',
|
|
12
|
+
status: '状态',
|
|
13
|
+
active: '活跃',
|
|
14
|
+
total: '总数',
|
|
15
|
+
mainThread: '主线程',
|
|
16
|
+
scheduler: '调度器',
|
|
17
|
+
running: '运行中',
|
|
18
|
+
stopped: '已停止'
|
|
19
|
+
},
|
|
20
|
+
tabs: {
|
|
21
|
+
tasks: '任务列表',
|
|
22
|
+
timeline: '时间线'
|
|
23
|
+
},
|
|
24
|
+
list: {
|
|
25
|
+
idTags: 'ID / 标签',
|
|
26
|
+
status: '状态',
|
|
27
|
+
driver: '驱动',
|
|
28
|
+
driverWorker: 'Worker (Web Worker)',
|
|
29
|
+
driverMain: '主线程 (setTimeout)',
|
|
30
|
+
schedule: '调度规则',
|
|
31
|
+
count: '次数',
|
|
32
|
+
lastRun: '最后运行',
|
|
33
|
+
actions: '操作',
|
|
34
|
+
noTags: '(无标签)',
|
|
35
|
+
tip: '✨ 提示: 点击行查看详情和历史记录。'
|
|
36
|
+
},
|
|
37
|
+
detail: {
|
|
38
|
+
back: '返回',
|
|
39
|
+
details: '任务详情',
|
|
40
|
+
config: '配置',
|
|
41
|
+
history: '执行历史',
|
|
42
|
+
lastRuns: '最近 {n} 次运行',
|
|
43
|
+
avgDuration: '平均耗时',
|
|
44
|
+
startTime: '开始时间',
|
|
45
|
+
duration: '耗时',
|
|
46
|
+
drift: '偏差',
|
|
47
|
+
status: '状态',
|
|
48
|
+
noHistory: '暂无执行历史',
|
|
49
|
+
noTask: '未选择任务',
|
|
50
|
+
success: '成功',
|
|
51
|
+
failed: '失败',
|
|
52
|
+
error: '错误'
|
|
53
|
+
},
|
|
54
|
+
timeline: {
|
|
55
|
+
zoom: '缩放',
|
|
56
|
+
timeRange: '时间范围: 最近 {n}秒',
|
|
57
|
+
legend: '图例',
|
|
58
|
+
instant: '瞬间',
|
|
59
|
+
duration: '耗时',
|
|
60
|
+
workerDriver: 'Worker 驱动',
|
|
61
|
+
mainDriver: '主线程 驱动'
|
|
62
|
+
},
|
|
63
|
+
status: {
|
|
64
|
+
running: '执行中',
|
|
65
|
+
paused: '已暂停',
|
|
66
|
+
stopped: '已停止',
|
|
67
|
+
idle: '调度中',
|
|
68
|
+
error: '错误'
|
|
69
|
+
},
|
|
70
|
+
actions: {
|
|
71
|
+
trigger: '立即触发',
|
|
72
|
+
start: '启动任务',
|
|
73
|
+
stop: '停止任务',
|
|
74
|
+
remove: '删除任务'
|
|
75
|
+
}
|
|
76
|
+
};
|