Skip to content

Commit efeec3b

Browse files
committed
fix: harden tool-list, dashboard, and CI after code review
Fix tool-list logic bugs, unify Chinese docstrings, add pytest/ruff CI, harden dashboard PATCH handling, and remove tracked compile artifacts.
1 parent 3770727 commit efeec3b

38 files changed

Lines changed: 687 additions & 482 deletions

.editorconfig

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
indent_style = space
9+
indent_size = 4
10+
11+
[*.{md,markdown}]
12+
trim_trailing_whitespace = false
13+
14+
[*.{yaml,yml}]
15+
indent_size = 2
16+
17+
[*.{c,cpp,h}]
18+
indent_style = tab
19+
indent_size = 4
20+
21+
[Makefile]
22+
indent_style = tab

.github/workflows/ci.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master, main]
6+
pull_request:
7+
branches: [master, main]
8+
9+
jobs:
10+
lint-and-test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.11"
19+
cache: pip
20+
cache-dependency-path: requirements-dev.txt
21+
22+
- name: Install dependencies
23+
run: pip install -r requirements-dev.txt
24+
25+
- name: Ruff
26+
run: ruff check scripts dashboard tests tool-list
27+
28+
- name: Pytest
29+
run: python -m pytest tests/ -q
30+
31+
compile-c:
32+
runs-on: ubuntu-latest
33+
34+
steps:
35+
- uses: actions/checkout@v4
36+
37+
- name: Install build tools
38+
run: sudo apt-get update && sudo apt-get install -y gcc g++
39+
40+
- name: Compile C/C++ tool-list demos
41+
run: |
42+
set -euo pipefail
43+
gcc -Wall -Wextra -o /tmp/fastsort \
44+
tool-list/algorithms/fastsort/fastsort.c
45+
gcc -Wall -Wextra -o /tmp/greedy \
46+
tool-list/algorithms/greedy/greedy.c
47+
gcc -Wall -Wextra -o /tmp/arrtoheap \
48+
tool-list/algorithms/heapsort/arrtoheap.c
49+
gcc -Wall -Wextra -o /tmp/heapmax \
50+
tool-list/algorithms/heapsort/heapmax.c
51+
gcc -Wall -Wextra -o /tmp/inittree \
52+
tool-list/algorithms/heapsort/inittree.c
53+
gcc -Wall -Wextra -o /tmp/nil \
54+
tool-list/algorithms/datastruct/nil_linklist.c
55+
g++ -std=c++11 -Wall -Wextra -o /tmp/strSplit \
56+
tool-list/algorithms/cpp/string/strSplit.cpp
57+
58+
- name: Smoke-run compiled binaries
59+
run: |
60+
/tmp/fastsort | tail -1
61+
/tmp/greedy | head -1
62+
/tmp/arrtoheap | head -1
63+
/tmp/nil
64+
/tmp/strSplit | tail -1

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Python
22
__pycache__/
3+
.pytest_cache/
34
*.py[cod]
45
*.pyo
56
.Python
@@ -68,3 +69,12 @@ dashboard/data.json
6869
*.out
6970
*.exe
7071
*.dSYM/
72+
73+
# Compiled C/C++ demos under tool-list (regenerate with gcc/g++)
74+
tool-list/algorithms/cpp/string/strSplit
75+
tool-list/algorithms/datastruct/nil
76+
tool-list/algorithms/fastsort/fastsort
77+
tool-list/algorithms/greedy/greedy
78+
tool-list/algorithms/heapsort/heapmax
79+
tool-list/algorithms/heapsort/arrtoheap
80+
tool-list/algorithms/heapsort/inittree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,16 @@ python3 scripts/build_dashboard_stats.py
4343

4444
## 依赖
4545

46-
Python 3.10+,`pip install -r requirements.txt`
46+
Python 3.10+:
47+
48+
```bash
49+
pip install -r requirements.txt # 运行脚本与 SMO
50+
pip install -r requirements-dev.txt # 开发:pytest、ruff
51+
```
52+
53+
本地检查(与 CI 一致):
54+
55+
```bash
56+
ruff check scripts dashboard tests tool-list
57+
python3 -m pytest tests/ -q
58+
```

dashboard/server.py

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import json
77
import subprocess
88
import sys
9+
import threading
910
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
1011
from pathlib import Path
1112
from typing import Any
12-
from urllib.parse import urlparse
13+
from urllib.parse import unquote, urlparse
1314

1415
ROOT = Path(__file__).resolve().parents[1]
1516
SCRIPTS = ROOT / "scripts"
@@ -26,6 +27,13 @@
2627
SOLVED_PATH = CATALOG_DIR / "solved-list.yaml"
2728
PORT = 8765
2829

30+
# Allowed status values per catalog (see catalog/schema.md).
31+
READING_STATUSES = {"inbox", "active", "done", "archived"}
32+
PROBLEM_STATUSES = {"open", "wip", "blocked", "deferred", "solved"}
33+
34+
# Serialize read-modify-write on YAML files (ThreadingHTTPServer is multi-threaded).
35+
_write_lock = threading.Lock()
36+
2937

3038
def refresh_data() -> None:
3139
subprocess.run(
@@ -35,14 +43,23 @@ def refresh_data() -> None:
3543
)
3644

3745

46+
def extract_id(path: str, prefix: str, suffix: str) -> str | None:
47+
"""Pull the URL-decoded id out of /<prefix>/<id>/<suffix> style paths."""
48+
if not (path.startswith(prefix) and path.endswith(suffix)):
49+
return None
50+
middle = path[len(prefix) : -len(suffix)].strip("/")
51+
return unquote(middle) or None
52+
53+
3854
def update_item_status(catalog: Path, item_id: str, status: str) -> bool:
39-
items = load_yaml(catalog)
40-
for item in items:
41-
if item.get("id") == item_id:
42-
item["status"] = status
43-
item["updated_at"] = today()
44-
save_yaml(catalog, items)
45-
return True
55+
with _write_lock:
56+
items = load_yaml(catalog)
57+
for item in items:
58+
if item.get("id") == item_id:
59+
item["status"] = status
60+
item["updated_at"] = today()
61+
save_yaml(catalog, items)
62+
return True
4663
return False
4764

4865

@@ -80,21 +97,15 @@ def do_GET(self) -> None:
8097
if not DATA_JSON.is_file():
8198
refresh_data()
8299
return self._send_json(200, json.loads(DATA_JSON.read_text(encoding="utf-8")))
83-
if path.startswith("/api/reading/") and path.endswith("/detail"):
84-
item_id = path.split("/")[3]
85-
for item in load_yaml(READING_PATH):
86-
if item.get("id") == item_id:
87-
return self._send_json(200, item)
88-
return self._send_json(404, {"error": "not found"})
89-
if path.startswith("/api/solved/") and path.endswith("/detail"):
90-
item_id = path.split("/")[3]
91-
for item in load_yaml(SOLVED_PATH):
92-
if item.get("id") == item_id:
93-
return self._send_json(200, item)
94-
return self._send_json(404, {"error": "not found"})
95-
if path.startswith("/api/problem/") and path.endswith("/detail"):
96-
item_id = path.split("/")[3]
97-
for item in load_yaml(PROBLEM_PATH):
100+
for prefix, catalog in (
101+
("/api/reading/", READING_PATH),
102+
("/api/solved/", SOLVED_PATH),
103+
("/api/problem/", PROBLEM_PATH),
104+
):
105+
item_id = extract_id(path, prefix, "/detail")
106+
if item_id is None:
107+
continue
108+
for item in load_yaml(catalog):
98109
if item.get("id") == item_id:
99110
return self._send_json(200, item)
100111
return self._send_json(404, {"error": "not found"})
@@ -113,16 +124,18 @@ def do_PATCH(self) -> None:
113124
if not status:
114125
return self._send_json(400, {"error": "status required"})
115126

116-
if path.startswith("/api/reading/") and path.endswith("/status"):
117-
item_id = path.split("/")[3]
118-
if update_item_status(READING_PATH, item_id, status):
119-
refresh_data()
120-
return self._send_json(200, {"ok": True, "id": item_id, "status": status})
121-
return self._send_json(404, {"error": "not found"})
122-
123-
if path.startswith("/api/problem/") and path.endswith("/status"):
124-
item_id = path.split("/")[3]
125-
if update_item_status(PROBLEM_PATH, item_id, status):
127+
for prefix, catalog, allowed in (
128+
("/api/reading/", READING_PATH, READING_STATUSES),
129+
("/api/problem/", PROBLEM_PATH, PROBLEM_STATUSES),
130+
):
131+
item_id = extract_id(path, prefix, "/status")
132+
if item_id is None:
133+
continue
134+
if status not in allowed:
135+
return self._send_json(
136+
400, {"error": f"invalid status; allowed: {sorted(allowed)}"}
137+
)
138+
if update_item_status(catalog, item_id, status):
126139
refresh_data()
127140
return self._send_json(200, {"ok": True, "id": item_id, "status": status})
128141
return self._send_json(404, {"error": "not found"})

docs/TODO.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
设计见 [UI-DESIGN.md](UI-DESIGN.md)**v0.2 已确认**)。
1515

16-
- [ ] P0:`scripts/build_dashboard_stats.py`(指标 + `dashboard/stats.json`
17-
- [ ] P1/v1:`dashboard/server.py` + 前端
16+
- [x] P0:`scripts/build_dashboard_stats.py`(指标 + `dashboard/data.json`
17+
- [x] P1/v1:`dashboard/server.py` + 前端
1818
- 默认 **阅读看板**`archived` **默认折叠**
1919
- Tab:阅读 / 问题 / **题解**;顶栏 **`solved_count`**
2020
- **拖拽**改 reading/problem `status` 并写回 YAML
@@ -36,3 +36,5 @@
3636
- [x] Chrome 书签 / 历史 / 会话只读导入
3737
- [x] reading 指标(classify)与 reading → problem 转化
3838
- [x] 四清单框架与开源文档(SVG 流程图)
39+
- [x] Dashboard 指标聚合(`build_dashboard_stats.py``dashboard/data.json`
40+
- [x] Dashboard 服务与前端(看板、题解 Tab、拖拽改状态写回 YAML)

requirements-dev.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-r requirements.txt
2+
pytest>=8.0
3+
ruff>=0.8

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
pyyaml>=6.0
2+
numpy>=1.24
3+
# 可选:tool-list/.../unionFind.py 的渗流可视化、部分 legacy 脚本
4+
# matplotlib>=3.7

ruff.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Ruff config for maintained Python code (scripts, dashboard, tests, tool-list).
2+
# legacy/ is excluded — archived solutions, not lint-gated.
3+
4+
target-version = "py310"
5+
line-length = 100
6+
7+
exclude = [
8+
"legacy",
9+
".pytest_cache",
10+
"problem-list",
11+
"reading-list",
12+
"catalog",
13+
"solved-list",
14+
"dashboard/data.json",
15+
]
16+
17+
[lint]
18+
select = [
19+
"E", # pycodestyle errors
20+
"F", # pyflakes
21+
"I", # isort
22+
"W", # pycodestyle warnings
23+
]
24+
ignore = [
25+
"E501", # line length — many legacy-style one-liners in tool-list
26+
]
27+
28+
# Algorithm teaching code often uses l/r bounds and compact if-bodies.
29+
[lint.per-file-ignores]
30+
"tool-list/algorithms/**" = ["E701", "E741"]

scripts/_catalog_utils.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,29 @@ def merge_by_key(
6969
continue
7070
if item_id in merged:
7171
base = merged[item_id]
72+
changed = False
7273
for k, v in item.items():
7374
if v in (None, "", [], {}):
7475
continue
76+
if k == "updated_at":
77+
continue
7578
if k == "tags" and isinstance(v, list):
76-
base["tags"] = sorted(set(base.get("tags", []) + v))
79+
new_tags = sorted(set(base.get("tags", []) + v))
80+
if new_tags != base.get("tags"):
81+
base["tags"] = new_tags
82+
changed = True
7783
elif k == "related" and isinstance(v, dict):
7884
rel = base.setdefault("related", {})
7985
for rk, rv in v.items():
80-
rel[rk] = sorted(set(rel.get(rk, []) + (rv or [])))
81-
else:
86+
merged_rel = sorted(set(rel.get(rk, []) + (rv or [])))
87+
if merged_rel != rel.get(rk):
88+
rel[rk] = merged_rel
89+
changed = True
90+
elif base.get(k) != v:
8291
base[k] = v
83-
base["updated_at"] = today()
92+
changed = True
93+
if changed:
94+
base["updated_at"] = today()
8495
else:
8596
item.setdefault("created_at", today())
8697
item["updated_at"] = today()

0 commit comments

Comments
 (0)