Commit Graph
348 Commits
Author SHA1 Message Date
tao.chen e186e5e541 refactor: use shadcn sidebar 2026-08-25 20:20:14 +08:00
tao.chen 9294c7fd9f update: use Button component in SchedulePage.tsx 2026-08-25 18:17:54 +08:00
tao.chen 4557fae037 fix: build error 2026-08-25 17:06:57 +08:00
tao.chen 3c79cd3a93 feat: add shadcn/ui 2026-08-25 16:58:23 +08:00
tao.chen 6a0b62fa6b Merge remote-tracking branch 'aliyun/develop' into develop 2026-08-25 16:55:13 +08:00
xiaozhu 12111de788 update:css样式拆分 2026-08-25 16:52:10 +08:00
tao.chen e805853b28 chore: docs 2026-08-25 14:17:05 +08:00
tao.chen 76aa2dcb27 fix: notebook 2026-08-25 11:35:51 +08:00
tao.chen 799f4e2a09 Merge branch 'develop-2' into develop 2026-08-25 11:27:50 +08:00
tao.chenandClaude Fable 5 91031342b8 refactor(frontend): consolidate features & components into single tree
把 components/admin/* 和 components/platform/* 共 11 个文件
移到对应 features/<name>/ 下,components/ 只保留跨特性共享的
common/ 子目录(features/admin/* 之前和 components/admin/* 同名
共存,迫使 features/admin/AdminPages.tsx 出现 barrel,顺带消除了
barrel 副作用 CSS 丢失的隐患)。

新的目录约定:
  - features/<name>/  = 特性模块,所有页面 / Modal / state / hooks
                         / routes 都在内,无 components/<name>/ 并列
  - components/common/ = 仅放 ≥2 个特性共用的 widgets
                         (Icon、Sidebar、Toast、Topbar、WelcomePanel)

改动:
  - 11 文件 git mv (components/{admin,platform}/* → features/*)
  - 8 文件的 Icon 相对路径 '../common/Icon' → '../../components/common/Icon'
  - 2 文件补 admin.css side-effect import(UserManagementPage /
    ProjectManagementPage 之前依赖 AdminPages barrel)
  - 3 文件(引用方)更新 import:SystemAdminPage、DashboardRoute、
    ScriptsPage
  - 删除 features/admin/AdminPages.tsx barrel(无消费者)
  - 删除空目录 components/admin、components/platform

验证:
  - pnpm typecheck 通过
  - pnpm build 通过,CSS 体积 35.11 kB 与重构前一致(无样式增减)
  - routes.ts / routes/platform.tsx 路径未动,route id 不变

踩坑:
  第一轮把 '../common/Icon' 错改成 '../../../components/common/Icon',
  typecheck 报 11 个 Cannot find module。原因:features/admin/ 和
  components/admin/ 都是 depth 2,'../../X' 在两边都解析到 app/X,
  只有 '../X' 才需要多一层。正确改法是 '../../components/common/Icon'。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 11:27:26 +08:00
tao.chen baa1e04af6 refactor: scriptWorkspaceStore.ts 2026-08-25 11:12:22 +08:00
tao.chen 2ed8d7b2cc refactor: useSchedulesStore.ts 2026-08-25 10:48:29 +08:00
tao.chenandClaude Fable 5 80c92f7fd4 fix(frontend): restore login & auth-loading styles with Tailwind
提交 6b77018 把 frontend/app/app.css 从 112 行砍到 91 行,
删除了 .login-* 与 .auth-loading 选择器,但 login.tsx /
AuthContext.tsx 的类名没同步迁移,导致:

  - /login 页面无样式(背景渐变、卡片阴影、输入框焦点环全部失效)
  - AuthContext 初次加载时的旋转 spinner 无样式

两处都用 Tailwind 任意值 [..] 语法精确复刻原 CSS,精确到
原始色值(#1677ff、#eef5fb 等),不引入新的 CSS 文件,
不修改 app.css、路由配置或依赖。

Spinner 用 Tailwind 内置 animate-spin(1s)替代原 0.8s
keyframes auth-spin,视觉差异肉眼不可见,如需精确复刻可在
app.css @theme 加 --animate-auth-spin。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 10:35:27 +08:00
tao.chen 97c8fe212b update: replace Icon with lucide-react 2026-08-25 09:59:59 +08:00
tao.chen fbb4cc0f1b feat: add lucide-react Icon 2026-08-24 18:41:49 +08:00
tao.chen 27a0504079 feat: register tailwindcss plugins 2026-08-24 18:35:10 +08:00
tao.chen 6b7701852f refactor: style css -> tailwindcss 2026-08-24 18:34:43 +08:00
tao.chen 9ce0b106fa update: init tailwindcss 2026-08-24 17:34:55 +08:00
tao.chen 95b588b96c refactor: update artifact list style 2026-08-24 17:29:19 +08:00
tao.chen b8246b1e9b refactor: update artifact list style 2026-08-24 17:28:55 +08:00
tao.chen b87f58be60 update: delete schedule 2026-08-24 15:26:25 +08:00
tao.chen de6d975fe9 chore: remove print 2026-08-24 15:02:26 +08:00
tao.chen ce8020e68c chore: web volumes 2026-08-24 13:58:24 +08:00
tao.chen b8a1e462ca chore: web volumes 2026-08-24 13:57:46 +08:00
tao.chen f09c1912fa chore: initialContent 2026-08-24 11:30:14 +08:00
tao.chen e02c66a7a6 chore: web volumes 2026-08-24 11:26:58 +08:00
tao.chen ba8a6ff79a chore: encrypt_secret.py 2026-08-24 11:05:01 +08:00
tao.chen 8ff399712f chore: encrypt_secret.py 2026-08-24 10:59:07 +08:00
tao.chen ddb87f66ee chore: remove boto3 2026-08-24 10:37:29 +08:00
tao.chen 24f40b5dd7 test(common.storage): cover async-only registry + factory contract
The drop-sync refactor is now contractually enforced:

* create_storage({"mode": "sync"|"async"|<any>}) raises StorageConfigError
  -- the regression we most want to catch is someone "restoring" the
  sync shape by re-introducing mode= handling.
* create_storage() preserves caller's dict, returns an AsyncStorageBackend
  subclass, and wraps constructor TypeError into a useful StorageConfigError.
* build_storage_config emits no mode key on either local or s3 branch.
* register_backend() refuses to silently overwrite a different class on
  the same name (collision guard), and accepts same-class re-registration.
* registry surfaces the built-in local + s3 classes after import; the
  conflict-test cleanup pattern avoids leaking global state across tests.

Adds [tool.pytest.ini_options] (asyncio_mode=auto, testpaths=tests) so
uv run --package common pytest common/tests works from the workspace root.

17 new tests, all green. Backend suite still 136 passed.
2026-08-24 10:30:22 +08:00
tao.chen 6050880c98 refactor(common.storage): drop sync storage abstraction and example_usage
The async-only direction was already the only one used in production:
* create_storage never accepted mode=sync; build_storage_config always
  emitted mode=async; zero callers referenced StorageBackend / SyncData
  / S3StorageBackend.sync / LocalStorageBackend.sync anywhere.
* Drop the parallel sync base class, the sync concrete classes in
  backends/local.py and backends/s3.py, and the boto3 dependency.
* Drop example_usage.py (zero importers; demonstration code, not part
  of the public surface).
* Rename LocalAsyncStorageBackend -> LocalStorageBackend,
  S3AsyncStorageBackend -> S3StorageBackend to reflect the single
  remaining class per type.
* Tighten create_storage: any mode=... key now raises StorageConfigError
  with the new pointer (settings.storage_backend controls behavior).
* Cleanup call sites: schedule.application.service.build_object_store
  no longer passes mode=async to create_storage.
* Cosmetic touch-ups in backend/services/storage.py and
  common/config.py docstrings where they still said "boto3" instead of
  "S3 client".

Public API surface preserved: AsyncStorageBackend / ObjectMeta /
create_storage / build_storage_config / register_backend all keep
their names and call signatures. backend tests: 136 passed.
2026-08-24 10:29:08 +08:00
tao.chen 8fa6203303 fix: schedule result api 2026-08-21 19:42:42 +08:00
tao.chenandClaude b493907775 feat(scripts): 跨 owner 懒加载目录树 + 跨用户可见 workspace/public
修两个后端接口问题:
1) /api/v1/workspace-directories 返回为空,目录树结构消失
2) 同 workspace 内脚本/数据互相可见但默认排除 private

后端改动
--------
* list_scripts / list_resources / list_workspace_directories 新增
  owner_user_id 可选 query 参数;缺省 = 当前请求者本人(scope 到
  workspace/{me}/...),传值时 scope 到该 owner 的子树。前端根加载
  默认只见自己一级,其他成员以折叠分组呈现。
* visibility 过滤统一:非 admin 请求者只返回 owner==me 或
  visibility ∈ {workspace, public};admin 跳过。owner=me 含自己
  的 private,owner=other 只剩其 workspace/public,排除他人 private。
* create_workspace_directory 两个分支 visibility 默认 'public'
  (非 private),使跨 owner 目录树可见;响应新增 owner_user_id 字段。
* platform.list_members 鉴权从 system_admin_context 放宽为
  系统管理员或该 workspace 活跃成员(让普通用户也能渲染同
  workspace 成员名册,用于跨 owner 分组)。
* main.py 注册 platform 模块(随 list_members 改动补齐导入)。
* .env.example 同步 common/config.py 26 个字段。

前端改动
--------
* ScriptExplorer.memberScriptGroups 改由 members 列表播种分组,
  display_name 取 members.display_name;inferredDirectories 现在按
  owner_user_id 标记,统一跨 owner 目录渲染。删除脚本目录页头与
  树分组标题的工作副本数量角标。
* WorkspaceTree 新增 ownerUserId 透传到 store.toggleExpanded;
  仅"我"的分组 mount 时 auto-expand,他人分组默认折叠,展开才
  调 loadOwnerGroup / owner-scoped loadScripts / loadChildren。
* scriptWorkspaceStore 引入 namespaced cache key
  (ownerCacheKey = `${ownerUserId ?? me}:${path}`),loadedScriptPaths
  / loadedChildPaths / loadedOwnerGroups 全部按 owner 隔离;
  toggleExpanded 用 loadPath === undefined 区分 group 头与真实
  目录,修"他人子目录点击不触发接口"的 loadPath 前缀误判 bug。
* api.ts / AuthContext 透传 ownerUserId 给 listScripts /
  listResources / listWorkspaceDirectories。

文档
----
* API.md: §3.2 创建目录 visibility 默认 public + 响应加 owner_user_id;
  §3.3.1 GET directories 加 owner_user_id 参数 + 响应字段;
  §3.4 GET scripts 改写为 owner 作用域 + visibility 过滤语义;
  §五.1 GET data-resources 新增,同一套统一语义;
  §7 intro 例外 — GET members 对系统管理员或 workspace 活跃成员开放。
* DEVELOP.md: Code layout 重写以反映 backend api/services/clients/
  schemas 拆分 + schedule domain/scheduling/application/execution/
  infrastructure 拆分 + common 子包(auth/storage/backends);
  Configuration 系统补全 26 个 settings 字段;新增
  "Owner-scoping + visibility (cross-owner browsing)" 小节;
  Per-service dev 注释用 uv run 的源布局要求;Add a new DAG endpoint /
  storage bucket 路径改为 backend/src/backend/api/* 与 services/*。

测试
----
* test_list_scripts_parent_path.py /
  test_resources.py 补充 owner_user_id 参数化直接调用 + LIKE
  前缀断言(workspace/{owner}/... 前缀)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 19:26:53 +08:00
tao.chen 8cf4b53dd4 fix(scripts): workspace-wide parent_path listing + private visibility on reads
1. 同 workspace 互相可见(排除 private):
   - list_scripts / count_scripts 已 workspace-wide + visibility 过滤,但
     单条读取(get/content/latest-version/versions)不校验 visibility,非
     owner 猜 id 即可读他人 private 脚本。新增 script_can_view(与 data
     resources 的 can_view 对称)并在 get_script_row / latest_version 强制,
     private 对非 owner 返回 404。

2. parent_path 为空默认拉根路径文件:
   - 物理存储为 workspace/{user_id}/...,根 prefix 原来是 workspace/ +
     NOT LIKE workspace/%/%,所有文件都在两层被整体排除,list_scripts("")
     恒空。改为 workspace/%/,配合 LIKE workspace/%/% AND NOT LIKE
     workspace/%/%/% 返回各 owner 根级文件。

3. 非空 parent_path 跨 owner 查询:
   - 原来 workspace/foo/ 永远匹配不到 workspace/{uid}/foo/...,子目录
     懒加载返回空,其他用户目录点击无内容。改为 workspace/%/foo/(owner
     段通配,与 list_resources 一致),_ / % 仍按字面转义。

测试:更新前缀契约断言,新增 SQLite 行为测试(跨 owner 根/子目录、转义)
与 script_can_view / get_script 权限测试,133 passed。
2026-08-21 17:20:05 +08:00
tao.chen 5045f0ad8c chore: delete demo_auth_enabled from config 2026-08-21 16:20:17 +08:00
tao.chen 952b08bb63 chore: update docstring 2026-08-21 16:07:26 +08:00
tao.chen 02bda7cc60 Merge branch 'worktree-backend-restructure' into develop 2026-08-21 15:43:17 +08:00
tao.chenandClaude 9f6466335c docs(schedule): record the deliberate ExecutionResult dataclass upgrade
Review (2026-08-21) found that stage 1 promoted ExecutionResult from a
plain class to @dataclass(frozen=True) along the way. No caller mutates
or compares these objects by identity, so the only externally visible
change is structured log output. User opted to keep the upgrade.

- domain/execution.py module docstring: explicit note that the frozen +
  value-equality form is a deliberate enhancement, not a behavioral
  accident
- CLAUDE.md "Schedule service layering" lesson: add a "don't silently
  upgrade dataclass-ness during a structural-only refactor" note so
  future refactors copy class definitions verbatim unless they intend
  to tighten semantics explicitly

No code change; tests still 29 green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 15:38:31 +08:00
tao.chenandClaude bca239ed4b refactor(backend): split into api/ schemas/ services/ clients/ layers
4-phase restructuring of the previously flat backend/ package. Each
phase lands as a single squash commit so future bisects stay readable
per phase if needed.

## Phase 1 — move + shim (location-only, zero behavior change)
* git mv 14 files into api/ schemas/ services/ clients/ subpackages
  (history preserved via RM/R renames)
* New files: api/{admin,auth,dependencies,jupyter,platform,resources,
  scripts,storage}.py + api/schedules/{schedules,runs}.py
* New files: schemas/{auth,common,jupyter,platform,resources,
  schedules,scripts}.py
* New files: clients/{rclone,runtime,scheduler}.py
* Old paths kept as 1-line `from backend.<new> import *` shims so
  tests/main.py/importers kept working untouched
* schemas/__init__.py now re-exports from backend.schemas.<domain>

## Phase 2 — APIRouter prefix consolidation
* Every APIRouter() now carries its prefix (e.g. prefix="/api/v1/auth")
  and decorators are stripped of the redundant path prefix
* URL paths exposed to the frontend are byte-identical to before
* Affected: api/{auth,jupyter,admin,platform,resources,scripts,
  storage}.py + api/schedules/{schedules,runs}.py

## Phase 3 — first service-layer extraction
* backend.services.schedules.validate_dag moved out of api/
  (pure DAG validator, no Request/BackgroundTasks/DB)
* api/schedules/schedules.py now re-exports the symbol so existing
  4 callsites keep working unchanged
* Added backend/tests/test_validate_dag.py: 8 unit tests covering
  DAG_EMPTY, linear chain, diamond, cycle, self-edge, duplicate
  edge, orphan edge, multi-root ordering

## Phase 4 — delete shims + unify test imports
* Removed 14 flat shim files + schemas/__init__.py
* Migrated 5 test files (32 import sites) to new paths:
  backend.scripts.* → backend.api.scripts.*
  backend.resources.* → backend.api.resources.*
  backend.jupyter.* → backend.api.jupyter.*
  backend.runtime_client.* → backend.clients.runtime.*
  backend.schemas.UpdateScriptRequest → backend.schemas.scripts.*
* audit.py kept at backend.audit (main.py references it; not a
  shim, real code)

## Final structure
backend/src/backend/
  main.py, audit.py, __init__.py
  api/            (10 files: routes + 2 subpackage)
  schemas/        (7 files: Pydantic contracts)
  services/       (storage + schedules)
  clients/        (rclone, runtime, scheduler)

## Verification
* uv run python -m compileall backend/src backend/tests — clean
* uv run --package backend pytest backend/tests -q — 122 passed
  (114 → 114 → 122 → 122 across phases)
* grep -r 'from backend\.\(scripts\|resources\|...\)' backend/ — 0 hits
* git blame --follow still traces file origins through the renames

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 15:32:04 +08:00
tao.chenandClaude 80130c6d77 refactor(schedule): move orchestrator to application/ + review fixes (stage 8)
Follow-up to the layered refactor (review-driven):

- Move scheduling/orchestrator.py -> application/orchestrator.py
  (orchestrator is application-level coordination, not a cron-trigger
  primitive; matches the intended target tree)
- Migrate orchestrator re-exports from scheduling/__init__.py to
  application/__init__.py; scheduling/ now exposes only CronScheduler
- Rewrite imports + 5 mock.patch string targets in test_janitor.py and
  the orchestrator import in test_layering.py
- Update docstring refs in application/service.py + execution/worker.py
- Add 4 runner smoke tests (test_layering.py): _limited_log under-limit /
  empty-sentinel / above-MAX_LOG_BYTES truncation; execute_artifact
  rejects unsupported script_type with ValueError
- infrastructure/__init__.py re-exports SchedulerStorageClient so
  ``from schedule.infrastructure import SchedulerStorageClient`` is a
  stable top-level surface
- CLAUDE.md engineering note: extend the commit trail to 7476c27 and
  note the stage-8 orchestrator placement

Zero behavior change; schedule/pyproject.toml untouched. 29 tests green.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 15:26:27 +08:00
tao.chenandClaude 7476c27d81 test(schedule): add layer-boundary smoke tests (stage 7)
Pin the new five-layer contract so a later refactor can't silently break
an import surface or lifecycle:

- domain: ExecutionResult defaults, terminal/failed state-set invariants,
  naive_utc normalization (import-side-effect-free)
- infrastructure.storage: SchedulerStorageClient base64 upload via
  httpx.MockTransport (no live server; base_url required for relative URL)
- scheduling: CronScheduler start/close lifecycle, global trigger cleared
- application: SchedulerService wires cron + orchestrator + worker, handler
  dispatch table points at the wired NodeExecutor
- execution: schedule.notebook_runner shim re-exports the real main

25 schedule tests green; schedule/pyproject.toml untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 14:52:31 +08:00
tao.chenandClaude d594111601 refactor(schedule): cleanup flat files + document layering (stage 6)
- Delete the six orphaned flat modules (context/executor/orchestrator/
  scheduler/storage_client/worker) — all import sites already point at the
  layered packages; keep notebook_runner.py as the compatibility shim
- Clear __pycache__; fix stale docstring module refs in surviving files
- Subpackage __init__.py files re-export public symbols per layer
  (CronScheduler / DispatchOrchestrator / SchedulerService / NodeExecutor /
  SchedulerStorageClient / ExecutionResult / TERMINAL_NODE_STATES ...)
- CLAUDE.md engineering notes: add "Schedule service layering" section
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 14:47:14 +08:00
tao.chenandClaude c89132abc8 refactor(schedule): extract application/ layer (stage 5)
- Move service.py -> application/service.py (SchedulerService class name
  unchanged; build_object_store / build_storage_http_client move along)
- main.py imports schedule.application.service
- Rewrite worker.py's lazy `from schedule.service import build_object_store`
  and test_worker.py's mock patch string targets — same class of bug as the
  test_janitor patch strings (silent no-op until the old file is deleted)
- Delete flat service.py (orphaned; only docstring refs remain in
  orchestrator, cleaned up in stage 6)
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 14:42:56 +08:00
tao.chenandClaude c45a7a50a1 refactor(schedule): extract execution/ + runners in layered refactor (stage 4)
- Move worker.py -> execution/worker.py, executor.py -> execution/executor.py
  (byte-identical copies; import sites updated)
- Merge old execution.py + notebook_runner.py into
  execution/runners/notebook.py: subprocess CLI (main/emit_outputs) plus the
  in-process helpers (_execute_notebook/_execute_python/execute_artifact)
- schedule/notebook_runner.py becomes a compatibility shim so
  `python -m schedule.notebook_runner` (the worker's stable -m string) still works
- Delete flat execution.py (shadowed by the new execution/ package)
- Zero behavior change; schedule/pyproject.toml untouched

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 14:33:43 +08:00
tao.chenandClaude 5501b26628 refactor(schedule): extract scheduling/ layer (scheduler + orchestrator)
Stage 3 of the layered refactor. Relocate the two scheduling components
into their own package so that domain / application / scheduling /
execution / infrastructure boundaries actually exist on disk.

- Add schedule/src/schedule/scheduling/__init__.py
- Move scheduler.py (232 lines) -> scheduling/scheduler.py
  (byte-identical via diff; CronScheduler class name unchanged)
- Move orchestrator.py (946 lines) -> scheduling/orchestrator.py
  (byte-identical via diff; DispatchOrchestrator + event constants
  unchanged; NOT further split this round, per plan)
- service.py lines 39-40: import paths rewritten to the new module
- tests/test_janitor.py: rewrite the import + 5 patch() string targets

  The 5 patch() targets ("schedule.orchestrator.session_scope" x3,
  "schedule.orchestrator.asyncio.sleep" x2) were NOT caught by the
  import-line grep — they patch module attributes at runtime and would
  have become dead no-ops after the move (and would hard-raise once
  the old module is deleted in stage 6). Rewriting them to
  "schedule.scheduling.orchestrator.*" keeps the janitor tests meaningfully
  exercising the new module.

- old flat scheduler.py / orchestrator.py left on disk; stage 6 deletes
  them once all layers are extracted.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- grep 'from schedule.(scheduler|orchestrator)\\b' (old paths): 0 matches
- grep '"schedule.orchestrator.' (old patch targets): 0 matches
- main.py / worker.py / domain/ / infrastructure/ / pyproject.toml
  byte-identical to HEAD

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 14:17:47 +08:00
tao.chen d68055a96c feat(scripts/resources): align list_scripts visibility with list_resources
69a9a48 把 data resources 的可见性从 list 恒真改成
workspace-wide + visibility 过滤 + admin 短路,但 scripts 端
没动。两端不对称,导致:

* 非 admin 调 list_scripts 走 user_relative_path →
  workspace/{当前用户ID}/...,永远拿不到别人的脚本
* count_scripts 同样 user-scoped,dashboard "全部脚本"
  只统计自己
* list_resources 响应没带 owner_display_name,data-only
  owner 的目录名回退到 userId.slice(-6),显示不友好

修复:
* list_scripts / count_scripts 改 workspace-wide(新建
  _build_list_scripts_workspace_descendant_prefix helper;
  旧 _build_list_scripts_descendant_prefix 保留标 deprecated
  避免破坏其它调用方);非 admin 追加
  or_(owner_user_id = me, visibility in {workspace, public})
  与 list_resources 完全对称;admin 短路
* resource_payload 加 owner_display_name 字段(与
  script_payload 对称);list_resources SELECT 加
  Users.display_name + outerjoin
* 前端 ScriptExplorer displayName 回退链:scripts 的
  owner_display_name → data resources 的 owner_display_name
  → 本人 user.display_name → userId.slice(-6) 占位
  ;ResourceItem 类型同步加 owner_display_name?: string|null

存储物理布局仍是 workspace/{user_id}/...,仅读取侧
listing/count 跨 owner。
2026-08-21 14:06:45 +08:00
tao.chenandClaude 190c672e42 refactor(schedule): extract infrastructure/storage/ layer
Stage 2 of the layered refactor. Move the storage HTTP client one
package deeper so that infrastructure code lives under a dedicated
namespace.

- Add schedule/src/schedule/infrastructure/__init__.py
- Add schedule/src/schedule/infrastructure/storage/__init__.py
- Add schedule/src/schedule/infrastructure/storage/client.py
  (verbatim copy of old schedule/src/schedule/storage_client.py,
  byte-identical via diff — 2682 bytes)
- main.py line 17: import path rewrite to the new module
  (only consumer — service.py and worker.py take storage_client as
  an `Any` constructor param and never imported the class)
- old schedule/src/schedule/storage_client.py left on disk; stage 6
  deletes it once all layers are extracted.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- grep 'from schedule.storage_client' (old path): 0 matches
- service.py and worker.py byte-identical to HEAD
- main.py / pyproject.toml / tests/ unchanged apart from the 1 import line

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 13:59:39 +08:00
tao.chenandClaude 3118694e66 refactor(schedule): add domain/ package files
Add the three files that complete stage 1 of the layered refactor:
- schedule/src/schedule/domain/__init__.py (empty package marker)
- schedule/src/schedule/domain/context.py
  (TERMINAL_NODE_STATES / FAILED_NODE_STATES / TERMINAL_RUN_STATES / naive_utc —
  pure types, no I/O)
- schedule/src/schedule/domain/execution.py
  (ExecutionResult dataclass, frozen=True)

The corresponding import-path rewrites in worker.py / orchestrator.py /
scheduler.py / execution.py were already landed in cdcfcb2 (the prior
commit on this branch). This commit only adds the missing domain/
package files those imports point at.

Validation:
- uv run --package schedule pytest schedule/tests -q: 18 passed
- uv run python -m compileall schedule/src: zero errors
- from schedule.domain.context / schedule.domain.execution importable
- main.py / pyproject.toml / tests/ unchanged

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 13:56:19 +08:00
tao.chen cdcfcb2e43 feat(audit): skip audit log for excluded health/root paths
健康检查与根路径(/health/live、/health/ready、/api/v1/health、
/、/health/storage)没有用户、没业务动作,每秒被 K8s/LB
探针刷一次只会灌进无意义噪音。命中排除集即跳过审计行;
诊断日志(method/path/status/ms 走 stderr)照常打,对容器
运维排错仍有用。

* settings.audit_excluded_paths: list[str] 默认覆盖 5 条
  基础设施路径,env AUDIT_EXCLUDED_PATHS 用逗号分隔
  (pydantic NoDecode + field_validator 兼容 str/list)
* main.py 模块级 _AUDIT_EXCLUDED = frozenset(...),
  access_log 的 success/exception 两条审计行各加守卫
  诊断无条件打
* 测试用 _AccessLogReplica 复刻 access_log 契约(不 import
  真实 main.py),新增 4 个 case:排除根路径、排除 /health/live、
  不排除路径照写审计、自定义排除集

顺带 schedule 模块:ExecutionResult 与 context 已迁到
schedule.domain.*(execution.py / orchestrator.py /
scheduler.py / worker.py),调用点跟进;schedule 自身
18 个测试在改前改后均通过。
2026-08-21 13:47:44 +08:00
tao.chen d2c87de32c refactor(audit): fold audit logging into access_log, drop AuditMiddleware
按用户复查意见把审计折进 main.py 已有的 access_log 中间件,
少一个 middleware、让诊断与合规共用一个出口。

* 删 backend.audit.AuditMiddleware(user_id 解析搬到 main.py
  的 _audit_user_id 模块级 helper,cookie/Bearer 头 + JWT
  验签,失败/缺失一律 '-',全异常捕获不让审计拖死业务)。
* access_log 在 success 路径补 logger.bind(user_id, method,
  path, status).info('audit');exception 路径同样补一条
  status=500 的审计行,然后 re-raise 让全局 handler 转 500。
* 删 app.add_middleware(AuditMiddleware)。
* audit.py 只剩 _DailyFileSink / configure_audit_logging /
  AUDIT_LOG_FORMAT,文件 sink 与 retention 清理逻辑不变。
* 测试改用 _AccessLogReplica 复制 access_log 审计契约(不
  import main.py 避免触发 lifespan 里的 MySQL/engine 初始化),
  删 3 个 middleware 单独 case,加 access_log 端到端 success /
  5xx / 无 JWT 三个 case。
2026-08-21 13:35:44 +08:00