1. 19 Jul, 2026 7 commits
    • Administrator's avatar
      refactor(master-data): useCrudList composable + 4 view decomposition — Candidate A · efd558d6
      Administrator authored
      
      
      Extract src/composables/useCrudList.js: owns list state + dialog state
      machine + applySaved/applyDeleted + 5 toggle methods. Config:
      { list, create, update, deleteFn, idKey, mapRow?, loadErrorKey? }.
      
      Decompose 4 master-data views into table + edit-dialog + delete-dialog
      SFCs (11 new files), mirroring the StoryView pattern proven in
      Candidate B. View shrinks to pure wiring of useCrudList + useFlashMessage
      + child SFCs.
      
      Structural fixes (now composition-enforced, not patch-enforced):
      - D5: applySaved merges backend response into local list, so audit
        fields (created_by, creation_date, last_updated_by, last_update_date)
        are visible immediately after save, no refresh required.
      - D7: useCrudList owns deleteError; only *DeleteDialog.vue reads it.
        Cross-dialog delete-error bug fixed by composition.
      
      Net change:
        SubjectView  763  →  85
        TermView     619  →  95
        StudentView 1312  → 115
        StoryView    173  →  85
        Total       2867  → 380  (-2487 lines)
      
      Tests: useCrudList.spec.js covers fetch (success/error/auto-clear/
      mapRow/non-array fallback), applySaved (create/update/audit-preservation/
      no-id/null-undefined), applyDeleted, 5 toggle methods, clearDeleteError,
      idKey+mapRow together. 23 new tests, 214 total passing.
      
      Other quality wins: ~30 console.log calls in the 3 large views removed;
      view-owned reference data (R2) keeps useCrudList agnostic of students/
      subjects/terms.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      efd558d6
    • Administrator's avatar
      refactor(story): decompose StoryView 763→173 lines — Candidate B · 7f6e1ee8
      Administrator authored
      
      
      StoryView was the last "master-data" god-view whose script section
      still owned state, fetch, CRUD methods, table config, and three dialog
      templates in one file. Mirrors the Candidate 2 TaskView decomposition
      pattern.
      
      - src/components/story/StoryTable.vue        — v-data-table + three
                                                      Map<id, name> computeds
                                                      for O(1) display lookup
                                                      (replaces O(n^2) find);
                                                      emits @edit / @delete
      - src/components/story/StoryEditDialog.vue   — create+edit merged via
                                                      nullable `story` prop;
                                                      owns save flow + inline
                                                      error alert
      - src/components/story/StoryDeleteDialog.vue — owns deleteStory call,
                                                      isDeleting, inline
                                                      error; preserves the
                                                      existing 48-line UX
      - src/composables/useFlashMessage.js         — { message, show, clear }
                                                      with built-in 3s
                                                      auto-dismiss
      - src/__tests__/composables/useFlashMessage.spec.js — 13 tests
                                                             (fake timers)
      
      Key design choices (settled via /grilling before implementation):
      
      1. Dialog seam shape is explicit props+events — :open, :story,
         @close, @saved, @deleted. Not v-model. Keeps multiple emits
         semantically distinct.
      
      2. Lookup data (students/subjects/terms) lives in StoryView as raw
         refs and is passed down to children. No useMasterData composable —
         that scope belongs to Candidate A (useCrudList).
      
      3. StoryEditDialog handles create+edit (TaskEditDialog pattern), with
         @saved carrying the backend response so the parent can do
         { ...existing, ...updated } merge and pick up audit fields
         (created_at, last_updated_at, etc.).
      
      4. StoryDeleteDialog owns the delete flow internally — parent just
         wires @deleted(id) to a list splice. The existing
         "close-edit-after-delete" contract is preserved via the parent's
         onStoryDeleted handler.
      
      5. Row-level delete is now in StoryTable (mdi-delete icon next to
         edit). Replaces the awkward "open edit -> click delete inside edit
         dialog -> confirm" three-click path with a two-click one.
      
      6. console.log calls in StoryView removed (match TaskEditDialog's
         console.error-only pattern; CLAUDE.md says services log heavily,
         not components).
      
      StoryView is now a thin orchestrator: refs, two fetch functions,
      open/close handlers, two merge handlers. fetchStories still depends
      on fetchRelatedData completing first (sequential await, NOT
      Promise.all — see comment in onMounted). Tests 178 -> 191.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      7f6e1ee8
    • Administrator's avatar
      style(locales): prettier sweep — trailing commas + line wrapping · 5ddf3dfd
      Administrator authored
      
      
      No semantic changes. Trailing commas in object literals, paren-spaced
      arrow params, line wrapping at column 80. Mirrored across en + zh-CN.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      5ddf3dfd
    • Administrator's avatar
      test: seam coverage for auth/authService/endpoints/lanAccess — Candidate 4 · 4bca327d
      Administrator authored
      
      
      Five new spec files take the test count 113 → 178 (+65). Zero
      production code changes; all coverage gain.
      
      - src/__tests__/utils/jwtFactory.js           — base64url JWT maker
      - src/__tests__/utils/authGuard.spec.js       — 21 tests on checkAuth()
                                                     (LAN bypass, already-authed,
                                                      refresh-failed, etc.)
      - src/__tests__/utils/lanAccess.spec.js       — 17 tests on real CIDR
                                                     parser (jsdom hostname
                                                     patching)
      - src/__tests__/api/endpoints.spec.js         — 10 tests, structural
                                                     lock for apiEndpoints
      - src/__tests__/api/authService.spec.js       — 5 tests, login +
                                                     refreshAccessToken HTTP
                                                     shape
      - src/__tests__/stores/authStore.spec.js      — 33 tests, first Pinia
                                                     spec; covers axios
                                                     interceptor 8-branch
                                                     decision tree including
                                                     the LAN GET-bypass that
                                                     had 0 coverage before
      
      Footnotes for future explorers:
      - auth.js reads localStorage at module-eval time to seed its initial
        ref values — vi.stubGlobal('localStorage', ...) installed in
        beforeEach is too late. Manipulate jsdom's real localStorage and
        clear() it in beforeEach instead.
      - The axios response interceptor's logout() call is closure-captured
        at setup time, so external reassignment of auth.logout won't
        intercept it. Assert logout side effects via state changes
        (auth.accessToken === null, etc.) instead.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      4bca327d
    • Administrator's avatar
      refactor(auth): pure checkAuth() seam; remove redundant init calls — Candidate 3 · cb14a136
      Administrator authored
      
      
      The router beforeEach guard was a 40-line inline function mixing auth
      state checks, LAN bypass, and route resolution. Pulled the decision
      logic into src/utils/authGuard.js — a pure function checkAuth(to,
      authState) → {allowed, reason, redirect?} — that takes the auth store
      as a plain object and returns a verdict. No Pinia, no vue-router, no
      DOM imports. Trivially testable (21 spec covering LAN bypass,
      already-authed-on-login, refresh-failed, etc.).
      
      Router guard shrinks to 5 lines; AppLayout drops a redundant
      router.push('/login') (auth.logout already navigates internally);
      5 master-data views drop redundant onMounted initializeAuth() calls
      (initializeAuth now runs once in main.js at boot).
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      cb14a136
    • Administrator's avatar
      refactor(task): decompose TaskView 2294→310 lines — Candidate 2 · 5975ab07
      Administrator authored
      
      
      TaskView was a god-view doing filter wiring, calendar rendering,
      edit-dialog state, image carousel, and image grid all inline. Pulled
      into focused modules:
      
      - src/components/task/TaskFilters.vue      — filter panel + emits update:*-id
      - src/components/task/TaskCalendar.vue     — FullCalendar wrapper
      - src/components/task/TaskEditDialog.vue   — create/edit dialog
      - src/components/task/TaskImageCarousel.vue — full-screen carousel
      - src/components/task/TaskImageGrid.vue    — thumbnail grid + upload
      - src/composables/useTaskFilters.js        — pure filter state + apply logic
      - src/utils/taskDisplay.js                 — completion/status display helpers
      
      Filter state now lives in useTaskFilters (covered by 20-test spec);
      TaskView is an orchestrator that mounts the children and forwards
      events. Closes the longest-standing "this file is too big to review"
      pain point.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      5975ab07
    • Administrator's avatar
      refactor(api): extract apiError/boolCodec/logger/paginate helpers — Candidate 1 · c5d94b56
      Administrator authored
      
      
      Five thin service adapters (student/subject/term/story/task) now route
      through three pure helpers:
      
      - apiError.js  — normalize axios errors (status, message, detail, original)
      - boolCodec.js — parse DRF Y/N/true/false/etc. into strict booleans
      - logger.js    — tag-prefixed console output (swap to no-op in tests)
      - paginate.js  — DRF paginated-list walker with page_size:100 + 100-page cap
      
      apiEndpoints lifted from src/api/index.js (was stale) into its own
      endpoints.js single source of truth. Service tests gain 4 new spec
      files (apiError/boolCodec/logger/paginate).
      
      The duplicated fetchAllPages() copy in studentService and taskService
      is now a single import; either delete the old code or accept the
      temporary duplication until a follow-up candidate collapses it.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      c5d94b56
  2. 18 Jul, 2026 3 commits
    • Administrator's avatar
      chore(test): add vitest setup · d6452cf0
      Administrator authored
      
      
      Wires up vitest + @vue/test-utils + jsdom so the spec 0004 tests can run.
      
      - package.json: 3 npm scripts (test, test:run, test:ui) + vitest /
        @vue/test-utils / jsdom devDeps.
      - vitest.config.js: jsdom env, @ alias pointing at src/, setup file.
      - src/test-setup.js: shared jest-dom-style matchers.
      - src/__tests__/smoke.test.js: sanity check the harness resolves the @
        alias and that taskService imports cleanly.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      d6452cf0
    • Administrator's avatar
      feat(task): spec 0004 — task images migrated to stdt_task_images sub-resource · 8925331e
      Administrator authored
      
      
      Task images were stored as a JSON-ish field on stdt_tasks. Spec 0004 (see
      docs/task-images-migration.md) splits them into a dedicated sub-resource
      stdt_task_images so each image is its own row with mime_type / file_name /
      etag / size_bytes, accessed via /api/tasks/<id>/images/.
      
      Frontend changes:
      - new useTaskImages composable owning image state per edit dialog, with
        eager-load priming in load() so thumbnails render and are clickable in
        the grid (fixes v-if branch gated on populated imageCache).
      - taskService refactored: listTaskImages / createTaskImage / deleteTaskImage
        / getTaskImageContent (blob fetch with responseType: 'blob' to dodge 406s).
      - apiEndpoints.TASKS.IMAGES added for documentation purposes only — services
        still hardcode paths (CLAUDE.md flags apiEndpoints as stale).
      - TaskView.vue: switched from inline task.images to useTaskImages(); image
        upload / preview / delete / carousel wired to the new composable. Explicit
        taskId passed to load() and commit() since useTaskImages is instantiated
        without a taskIdRef at module scope.
      - locales en/zh-CN: new image keys (addImage, pendingUpload, clickToLoad,
        imagePartialFailure, imageLimitReached, etc.).
      
      Tests:
      - src/__tests__/api/taskService.test.js
      - src/__tests__/composables/useTaskImages.test.js
      - src/__tests__/composables/useTaskImages.network.test.js
      
      Also: .claude/ added to .gitignore (Claude Code local permissions, per-dev).
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      8925331e
    • Administrator's avatar
      fix(AppLayout): pin background to viewport so page no longer scrolls past footer · 66e40940
      Administrator authored
      
      
      <v-img cover min-height="100dvh"> was forcing v-responsive to size itself
      to the image's natural aspect ratio (1692px wide × 2333/3500 ≈ 1128px tall),
      making the page 1128px tall on a 769px viewport. The bottom ~360px had no
      white overlay, so scrolling past the footer leaked the unwrapped image.
      
      Replace <v-img> with a position:fixed <div class="app-bg"> using
      background-image: cover, and move the white overlay from inline style to
      scoped .app-content CSS. Page scrollHeight dropped 1128 → 781; max scroll
      359 → 12 (just v-main's slight overflow).
      
      Verified: background still renders washed-out behind content, edit dialog
      opens correctly (pointer-events: none doesn't block v-overlay/v-dialog),
      all routes still work.
      Co-Authored-By: default avatarClaude <noreply@anthropic.com>
      66e40940
  3. 15 Jul, 2026 1 commit
  4. 06 Jul, 2026 3 commits
  5. 04 Jul, 2026 1 commit
    • Administrator's avatar
      added multiple feat · fd870ca4
      Administrator authored
      UI: add Settings skeleton page with v-tabs + 6 categories
      UI: enlarge TaskView event fonts and replace progress bar with SVG ring
      UI: add native title tooltip on TaskView events
      UI: convert filter panel toggle to floating icon button with slide animation
      UI: set browser tab title to "学生管理系统"
      fd870ca4
  6. 02 Jul, 2026 1 commit
  7. 04 Sep, 2025 1 commit
  8. 03 Sep, 2025 1 commit
  9. 02 Sep, 2025 1 commit
  10. 01 Sep, 2025 1 commit
  11. 30 Aug, 2025 1 commit
  12. 29 Aug, 2025 2 commits
  13. 28 Aug, 2025 1 commit
  14. 26 Aug, 2025 3 commits
  15. 19 Aug, 2025 2 commits
  16. 13 Aug, 2025 2 commits