diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31e7023..1d29f3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,30 @@ jobs: - name: Mypy run: uv run mypy py_src/taskito/ tests/python/ --no-incremental + dashboard-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: dashboard/package-lock.json + + - name: Install dependencies + run: cd dashboard && npm ci + + - name: Biome lint + format check + run: cd dashboard && npx biome ci src/ + + - name: TypeScript check + run: cd dashboard && npx tsc --noEmit + + - name: Build check + run: cd dashboard && npx vite build + rust-test: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 784423d..931ca15 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,8 @@ Thumbs.db htmlcov/ junk/ +# Dashboard +node_modules/ + # Docs site/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a91998b..3a02225 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,3 +35,17 @@ repos: language: system types: [python] pass_filenames: false + + - id: dashboard-lint + name: dashboard lint (biome) + entry: bash -c 'cd dashboard && npx biome ci src/' + language: system + files: ^dashboard/src/ + pass_filenames: false + + - id: dashboard-typecheck + name: dashboard typecheck + entry: bash -c 'cd dashboard && npx tsc --noEmit' + language: system + files: ^dashboard/src/ + pass_filenames: false diff --git a/dashboard/biome.json b/dashboard/biome.json new file mode 100644 index 0000000..a62ddd9 --- /dev/null +++ b/dashboard/biome.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.8/schema.json", + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "a11y": { + "noStaticElementInteractions": "off", + "useKeyWithClickEvents": "off" + }, + "complexity": { + "noForEach": "off" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always" + } + }, + "css": { + "linter": { + "enabled": false + }, + "formatter": { + "enabled": false + } + }, + "files": { + "includes": ["src/**/*.ts", "src/**/*.tsx"] + } +} diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..9d3be91 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,12 @@ + + +
+ + +| + + | + )} + {columns.map((col, i) => ( ++ {col.header} + | + ))} +
|---|---|
| + toggleRow(key)} + onClick={(e) => e.stopPropagation()} + class="accent-accent cursor-pointer" + /> + | + )} + {columns.map((col, ci) => ( ++ {typeof col.accessor === "function" + ? col.accessor(row) + : (row[col.accessor] as ComponentChildren)} + | + ))} +
{message}
+ {subtitle &&{subtitle}
} +Automatic failure protection status
+Failed jobs that exhausted all retries
+{job.task_name}
+Browse and filter task queue jobs
+Structured task execution logs
+Task performance and throughput
+Real-time queue status
+Monitor and control individual queues
+Worker dependency injection runtime
+Proxy reconstruction and interception metrics
++ {workers?.length ?? 0} active {"\u00b7"} {stats?.running ?? 0} running jobs +
+| ID | Task | Queue | Status | Priority | Progress | Retries | Created | -
|---|---|---|---|---|---|---|---|
| ${escHtml(j.id.slice(0, 8))} | -${escHtml(j.task_name)} | -${escHtml(j.queue)} | -${badgeHTML(j.status)} | -${j.priority} | -${progressHTML(j.progress)} | -${j.retry_count}/${j.max_retries} | -${fmtTime(j.created_at)} | -
| Time | Level | Message | Extra |
|---|---|---|---|
| ${fmtTime(l.logged_at)} | -${l.level} | -${escHtml(l.message)} | - -
| Replay Job | Replayed At | Original Error | Replay Error |
|---|---|---|---|
| ${escHtml(h.replay_job_id.slice(0, 8))} | -${fmtTime(h.replayed_at)} | - - -
| ID | Original Job | Task | Queue | Error | Retries | Failed At | Actions | -
|---|---|---|---|---|---|---|---|
| ${escHtml(d.id.slice(0, 8))} | -${escHtml(d.original_job_id.slice(0, 8))} | -${escHtml(d.task_name)} | -${escHtml(d.queue)} | - -${d.retry_count} | -${fmtTime(d.failed_at)} | -- |
| Task | Total | Success | Failures | Avg | P50 | P95 | P99 | Min | Max | -
|---|
| Time | Level | Task | Job | Message | Extra | -
|---|---|---|---|---|---|
| ${fmtTime(l.logged_at)} | -${l.level} | -${escHtml(l.task_name)} | -${escHtml(l.job_id.slice(0, 8))} | -${escHtml(l.message)} | - -
| Task | State | Failures | Threshold | Window | Cooldown | Last Failure | -
|---|---|---|---|---|---|---|
| ${escHtml(b.task_name)} | -${b.state} | -${b.failure_count} | -${b.threshold} | -${(b.window_ms / 1000).toFixed(0)}s | -${(b.cooldown_ms / 1000).toFixed(0)}s | -${b.last_failure_at ? fmtTime(b.last_failure_at) : '—'} | -