- Stable package release:
0.7.1(TenonAdmin+TenonAdmin.Excel+TenonAdmin.Workflow), locally validated and authorized for localdevintegration; live deployment unchanged - TenonAdmin source: tag
v0.7.1, commite0e6e8fb2d699a9f573e218d1c6b3ecc3baff6fc - README bootstrap template artifact:
TenonAdmin.Templates@0.7.1(verified NuGet metadata; not a runtime dependency) - Frontend base:
Tenon-Net/TenonAdmin/web#v0.6.0(c7da8a0cb1a92e04aecba370371e65b746a0dc7f); Workflow and required shared deltas migrated from fixedv0.7.0, then the exactv0.7.1definition permission delta; CRM/login/brand/proxy/tooling retained
P0 was first validated against 0.3.0; later bumps: v0.3.1, v0.3.2, v0.3.3, v0.5.0 (Excel + customer import/export dry-run), v0.6.0 (QA36 delegation, scoped user/org lists, job catalog, frontend re-extract).
This ledger owns the detailed implementation work for P1-P4. The strategic ledger and kernel/template dogfood intake remain in TenonAdmin.
Status: complete on 2026-07-24. See P0 validation.
- Confirm the generated PackageReference is exactly
TenonAdmin0.3.0. - Record
dotnet restoreand Release build evidence. - Record first SQLite startup, CodeFirst schema creation, and random super-admin password output.
- Complete a real HTTP login and record
/health,/health/ready, and/openapi/v1.jsonresponses. - Install frontend dependencies, generate the API contract, type-check, lint, and complete a real browser login.
- Reproduce the critical README path from an empty directory.
- Record consumer findings. Only reusable kernel/template findings are eligible for a TenonAdmin dogfood issue.
- Upgrade to
0.3.1and re-run every gate after the upstream fixes shipped, includingnpm ci,npm audit, tests, and the production build. - Upgrade to
0.3.2and adopt the shippedProperties/launchSettings.jsonfix in place of the interim environment-variable workaround; re-run every gate including a plaindotnet run. - Upgrade to
0.3.3(version-alignment only — theTenonAdmincore package is byte-identical to0.3.2; onlyTenonAdmin.Templates'Dockerfilechanged, and this repo already carried that fix independently).web/was deliberately not re-extracted since upstream made no changes there.
Status: complete on 2026-07-24.
- Add the CRM consumer module with
code=crm,ModuleId=1000, its default route, and menu ownership.Modules/Crm/CrmModuleSeed.csregisters theSysModulerow (Id=1000,DefaultRoute=/crm/customer,ApiPrefix=biz); no menu rows yet — those are seeded in P2, not code. - Add
Customeras aDataEntitywith name, contact, phone, intended amount, and status. No organization or creator anchors are set in service code; the runtime AOP fills them. - Implement models, service contract, service, consumer error-code constants from
60000, DI registration, and anapi/v1/biz/customercontroller guarded by[RolePermission].BizErrorCode.CustomerNotFound = 60001. - Keep
CustomerServicefree of manual organization IDs,CreateOrgId, and organizationWHEREclauses. Confirmed by static grep — the only match is the class's own doc comment stating the guarantee. - Add CRUD, paging, and data-scope write-guard integration tests.
tests/tenon-example.Tests(new xUnit project, rawServiceCollection+ directIDataScopeContext.Currentmutation, no HTTP layer — so[RolePermission]'s 403 never enters the picture): 12 tests, all green. Cross-orgGetAsync/UpdateAsync/DeleteAsyncall throwCustomerNotFound; same-org succeeds;PageAsyncexcludes out-of-scope rows. - Add
GET /api/v1/biz/customer/scopewith[RolePermission]. Returns a structuredCustomerScopeDto(Kind/OrgName/VisibleOrgCount/IncludeSelf, no text) computed fromIDataScopeContext.Current+ the full org tree (IOrgService.ListAsync()). Covered by 8 theory cases: unrestricted, root-and-descendants, single leaf org, disjoint leaves, a non-covering partial subtree (must not misclassify as "and below"), andIncludeSelfalone and combined.
Manual end-to-end verification: dotnet run (plain, no env override — proves the P0/0.3.2 launch-profile fix holds), real login, full add/get/page/update/delete/not-found cycle over HTTP with curl, and /openapi/v1.json exposing all five customer routes. Release build: 0 warnings, 0 errors.
Status: complete on 2026-07-24.
- Seed the fixed organization tree and customer counts: headquarters 214; South China 128 consisting of Shenzhen 42, Guangzhou 43, and Dongguan 43; North China Beijing 43 and Tianjin 43.
Modules/Crm/Seeds/CrmOrgSeed.cs(8 orgs, HQ → South/North → 5 branch leaves) +CrmCustomerSeed.cs(214 rows generated deterministically per branch, not hand-written literals). - Seed three non-super-admin trial users: headquarters
All, South ChinaOrgAndChildren, and ShenzhenOrg. All useDefaultModuleId=1000,MustChangePassword=false, a non-nullLastPasswordChangeTime, no phone binding, andEnabled=true.CrmUserSeed.cs; shared demo passwordTrial@123456(local/evaluation use only, hashed at rest).Org/OrgAndChildrenresolve against each user's ownOrgId(kernelDataScopeProviderconvention), so the org binding lives on the user, not the role. - Seed CRM module
code=crm,ModuleId=1000(P1'sCrmModuleSeed), default route, menu ownership (CrmMenuSeed.cs: one page node + 3 read-only button nodes), roles (CrmRoleSeed.cs), role data scopes (CrmRoleDataScopeSeed.cs), user roles (CrmUserRoleSeed.cs), and explicit GET permissions for customer page, detail, and scope endpoints granted to all three roles (CrmRoleMenuSeed.cs) — the three accounts differ only in data scope, not in feature permissions. - Keep consumer-owned fixed IDs in
1000-1999, unique within each entity (customers occupy1000-1213). All 9 seed classes are idempotentISeedDataimplementations (join tables declareDedupColumns);CrmCustomerSeedexplicitly setsCreateOrgId/CreateUserIdper row since there is no login context at seed time. - Prove two initializations of the same database do not drift. Prove the three accounts return
214,128, and42from the same customer page endpoint and cannot see cross-scope details; retain P1 write-guard coverage. Automated:CrmSeedIdempotencyTestsboots the realAddTenonAdminhost twice against the same SQLite file (soDatabaseInitializeractually runs, not a hand-rolledCodeFirst.InitTables), asserts identical row counts and an unchanged password hash across both runs, and asserts214/128/42after each. (Caught along the way:AddTenonAdminswaps inHttpContextDataScopeContext, whose setter silently no-ops outside a real HTTP request — a test-only footgun, not a product bug — worked around by re-registering the plain in-memoryIDataScopeContextafterAddTenonAdmin.)
Manual end-to-end verification: real dotnet run (26 entities, 429 seed rows on first boot, 0 new rows on a second boot against the same file), real logins as all three trial accounts, page/scope totals matching 214/128/42, and a cross-scope detail request from the Shenzhen account returning CustomerNotFound. 13/13 automated tests green; Release build 0 warnings, 0 errors.
Status: complete on 2026-07-24.
- Build the CRM list and form in
web/, including complete zh/en i18n, menu integration, and permission wiring. Do not add React. Business-module file placement perskills/create-crud-frontend.md(new files only, upstream-owned files untouched):types/crm.ts,api/crm.ts,locales/ext/{zh-CN,en-US}/crm.ts,views/crm/customer/index.vue. Menu integration needs no route code —CrmMenuSeed'sComponent="crm/customer/index"(seeded in P2) resolves directly to this file via the dynamic-route mechanism. - Compose the current range label from the scope DTO and the paging total. Do not hard-code account names or infer an original
ScopeTypefrom a merged context. The page callscustomerApi.scope()once on mount and rebuilds the label fromProTable's@loadedtotal — never from the logged-in account name. - Express all organizations, a root organization and descendants, one organization, specified organizations, and
IncludeSelfconsistently from the DTO in both locales.scopeLabel()switches onCustomerScopeKind(All/OrgAndChildren/Org/Specified/None) and composesIncludeSelforthogonally ({scope} + 本人/仅本人), matching the backend'sComputeScopesemantics exactly since both share the same five-way classification. - Verify generated API, type checking, linting, and real logins for the three accounts. Confirm the expected scope labels and totals
214 / 128 / 42.npm run gen:api+typecheck+lint+ productionbuildall clean. Real Playwright Chromium runs against the dev server for all three trial accounts confirmed the exact scope text (全部组织,华南大区 及以下,深圳分公司) and totals (214/128/42) in the rendered page, and — a gap the skill's own reference template doesn't cover — that新增/编辑/删除are hidden for all three (they hold only the three GET permissions seeded in P2) whilesuperAdminsees full CRUD after selecting the CRM tile from the multi-app portal (fail-open bypass).
Consumer finding (page-scoped, not a kernel bug): the SqlSugar/AspNetCore layer has no SignalR hub wired for the notification bell in this minimal consumer host, so the browser console logs repeated negotiation-404 errors after login. Pre-existing in the template, unrelated to CRM; not fixed here since it's out of P3's scope.
Status: complete on 2026-07-25. Deployed to production, replacing the kernel's own demo. Commit b39565e (docker fix + compose stack).
- Obtain separate explicit authorization before replacing any production domain or deployment, then execute it. User explicitly authorized the replacement in-session ("这是服务器信息...你替换掉吧") after the prior local-only decision. Deployed
tenon-example(dev@b39565e) viadocker compose(MySQL + Redis + backend + Caddy-fronted Vue) to154.12.54.208:/root/opt/tenon/tenon-example, staged on ports 8090/8091, verified end-to-end (health, all three logins,214/128/42row counts, DemoMode write-rejection), then cuttenonadmin.52moyu.net's host Caddy (/etc/caddy/Caddyfile) over to it and reloaded (systemctl reload caddy, zero downtime for the server's other domains). External verification:curl https://tenonadmin.52moyu.net/health→200,/health/ready→200,/login→200.- Backup taken before cutover:
/root/opt/tenon/backup-tenon-admin-20260725-050042/on the server — the old repo's uncommitted diff,.env,docker-compose.override.yml, the live host Caddyfile, and a fullmysqldump --all-databasesof the old stack's MySQL. The oldtenon-admincompose stack (containers + named volumes) was stopped, not removed, so it's still on disk. - Rollback (tested reversible, not just documented): revert
/etc/caddy/Caddyfilefrom the pre-cutover copy the script saved at/etc/caddy/Caddyfile.pre-tenon-example-cutover,systemctl reload caddy, thencd /root/opt/tenon/tenon && docker compose -p tenon-admin start(containers still exist, stopped —startbrings them back with their data intact, no rebuild needed).
- Backup taken before cutover:
- Configure public DemoMode, read-only CRM permissions, hidden write affordances, server-side write rejection, and a repeatable account-state recovery process.
TenonAdmin:DemoModeis a pre-existing kernel switch (DemoModeFilter), enabled via a server-localdocker-compose.override.yml(untracked, mirrors the old stack's own convention). Verified against the actual production deployment (not just locally): reads (login,page) return200;POST /api/v1/biz/customer/addreturns{"code":41002,"msgKey":"error.perm.demoReadOnly"}. Write affordances stay hidden for the three trial accounts (P3's permission gating, independent of DemoMode). Account-state recovery: reseeding is idempotent (P2); no live-reset process was built beyond that, since nothing so far has required one. - Provide the three trial accounts and a concise first-screen instruction showing how the same customer page changes by account. Documented in the README's "CRM Module" section (account/scope/row-count table + the shared password); now live at the accounts/passwords/URL below.
- Capture the three-account comparison and add the real-app entry point to documentation. Three screenshots (
docs/assets/{hq-admin-214,south-manager-128,shenzhen-specialist-42}.png) embedded in the README, one per account, each showing the scope banner and row count. Screenshots are from the local verification pass (P4 first round); not re-captured against the production URL since the rendered page is byte-identical. - Verify health, login, customer paging, rollback, and the two-minute unauthenticated discovery path. Health/login/paging verified against the live domain above. Rollback verified reversible (see above) but not executed end-to-end as a live drill (that would mean actually taking the new deployment down); the unauthenticated discovery path is simply visiting
https://tenonadmin.52moyu.net/loginand reading the account table in the README — no additional first-screen UI banner was built for it.
Deployment reference (server-side facts, for whoever operates this next):
- Host:
154.12.54.208(Ubuntu 22.04), a shared multi-tenant box running several unrelated services behind one host-level Caddy (/etc/caddy/Caddyfile) — touch only thetenonadmin.52moyu.netblock. - App:
/root/opt/tenon/tenon-example, a plaingit cloneof this repo'sdevbranch,docker compose -p tenon-example up -d --build. Secrets live in.envand the demo policy indocker-compose.override.yml(both untracked, server-local) — deliberately excluded from git the same way the old kernel demo's deployment was. The override pinsTenonAdmin__DemoMode=false,CrmDemo__ReadOnly=trueandCrmDemo__ImportDryRun=true. DemoMode has to stay off because it blocks the import wizard's POSTs (seev0.5.0-upgrade.md);ReadOnlyis the app's own gate (DemoReadOnlyFilter) that replaces it — same 403/41002, but login and/api/v1/biz/customer/importare allowlisted. That gate is what keeps the public demo safe, not the credentials: the three trial passwords are published in the README, and总部管理员genuinely holds 60 non-GET grants across system management (that is the point of the account).superAdminis deliberately kept out of the public demo — it bypasses[RolePermission], so its password is rotated and unpublished; the live one was rotated on 2026-08-16.ReadOnlyandImportDryRunmust both stay on — with only the first, the import wizard writes for real. - To redeploy after a
git pull:cd /root/opt/tenon/tenon-example && git pull && docker compose -p tenon-example up -d --build. - A found-and-fixed dogfood bug surfaced getting here: the template's
Dockerfilebroke for any hyphenated project name (tenon-example→tenon_exampleinside file content, but the actual.csproj/.dllkept the hyphen) — see the strategic ledger's dogfood section. Fixed in both repos; not yet cut into a TenonAdmin release.
Status: complete on 2026-07-25.
After P4 shipped, the user reviewed the live deployment and rejected its shape: logging in with any trial account showed exactly one menu (CRM), with no way to reach the kernel's stock admin console. Their expectation — matching this ledger's own framing of CRM as "the first flagship module," not the only one — was that the demo should show the kernel's full out-of-the-box capability (org/user/role/menu/dict/config/log/file management) with CRM layered on top.
Root cause (not a rendering bug): the kernel always seeds a built-in system SysModule (Id=1) with the full admin menu tree, but the three CRM trial roles were only ever granted SysRoleMenu rows for CRM's own 3 buttons. The frontend's module portal derives visible modules strictly from the calling user's menu grants, so with zero grants outside CRM, exactly one module was ever computed and the picker never appeared — a missing seed, not a bug in the portal itself.
Fix (grilled and decided with the user):
- Only HQ admin (
总部管理员) additionally gets full access to the kernel'ssystemmodule — every menu, every button (add/edit/delete included), granted as a realSysRoleMenurole, not asuperAdminbypass. New seedModules/Crm/Seeds/CrmHqAdminSystemMenuSeed.cs(105 menu grants, Ids 1100+, tied to the kernel'sDefaultMenuSeed.csat the version in use — won't auto-track future kernel menu additions). South China manager and Shenzhen specialist are unchanged, still CRM-only. - Publish the existing
superAdmincredentials as a fourth public account (its password was already fixed viaTenonAdmin:Seed:AdminPasswordat deploy time, not rotated —SuperAdminSeedonly sets the password on the very first boot, so changing.envon an already-seeded server would do nothing without a destructive reseed). superAdmin bypasses org-scope entirely and sees all three modules (system+crm+ the kernel's own samplebusinessmodule). - Deleted the never-wired
Modules/SampleDoc*scaffold (four files: entity, service interface/impl, controller) and itsProgram.csregistration — a template leftover with no menu/permission seed of its own, sitting as an empty, unused table. CodeFirst doesn't drop the now-orphanedsample_doctable; left in place, harmless. - Login page: added
superAdminas a fourth one-click quick-login entry (per-account password, since it differs from the sharedTrial@123456).
Verified via Playwright against a fresh local seed: HQ admin's /module picker now shows two tiles (系统 + 客户管理, CRM still the default); entering 系统 and navigating to /system/user renders the full user-management page with 新增/编辑/批量删除 etc. all visible and functional (DemoMode is off locally, so these are real, working buttons, not dead UI) — a real granted role, not fail-open. South manager and Shenzhen specialist re-verified unchanged (128/42, single CRM module). superAdmin's /module picker shows all three tiles. dotnet build and npm run typecheck/lint clean. Redeployed to production (docker compose -p tenon-example up -d --build app web) and re-verified against the live domain.
Each completed task uses its own English conventional commit. This application keeps CRM business work local. A possible kernel or satellite-package improvement requires independently documented dogfood evidence; it is not authorized merely by this ledger.
Authorized scope: dependency upgrade and framework Workflow integration only. No customer-change approval, second consumer module, Integration package, AI provider, production writes or deployment. Historical P0 limits above do not remove the CRM already shipped. Global agent rules were not changed.
- Baseline: fetched
origin/dev=2bf90a0d55d9c40936f2453ef66aa05eb1fc7627; local branchvalidate/tenonadmin-0.7.0. - Core/Excel upgraded and CRM regressed before adding Workflow; all three packages pinned to stable
0.7.0. - Existing CRM registrations, seed IDs/scopes, import dry-run, exports and
DemoReadOnlyFilterretained. Workflow keeps framework menu IDs48000..48099and module ownership1/2; no trial-role grants added. - Vue workflow plus required public seams adopted from the exact tag; contract generated from this consumer host, not copied from MinimalHost.
- Validation details, failures, boundaries and database upgrade steps: 0.7.0 upgrade record.
- Backend: 32 tests passed; Vue: 202 unit tests passed. SQLite and isolated MySQL 8.0.46 fresh/old-data upgrade, CRM scopes, approve/reject, restart persistence and demo HTTP protection passed.
- Browser: 17 passed / 1 failed. Ordinary employee start/approve/history and all nine menu components passed; the ordinary manager designer fails because published definition menu permissions use
{id}but controller routes use{id:long}. HTTP detail/version reproduction also returns 403/41001. Partially complete; not merge-ready. No framework/security bypass was added; failing checks are retained. - At this historical validation gate, changes were local and uncommitted, not a pushed release or a live-demo upgrade. Customer-change approval remains the next separately authorized business task.
Continuation of the preceding local upgrade, not a production release. The 0.7.0 403 remains a historical reproduced defect; current acceptance uses the published fix.
- Branch
validate/tenonadmin-0.7.1, same verified dev baseline2bf90a0d55d9c40936f2453ef66aa05eb1fc7627; prior local work and original.omx/retained. - Core/Excel/Workflow and all seven resolved TenonAdmin packages are exactly
0.7.1; sourcev0.7.1/e0e6e8fb2d699a9f573e218d1c6b3ecc3baff6fc. Only the matching Vue definition/detail-delete permission checks changed beyond the preceding migration. - Existing ordinary manager grants survive SQLite 0.6.0/0.7.0 upgrades; detail/version reads now succeed, ungranted users remain denied. Approval/rejection history, pending tasks, CRM ID sets/scopes, custom menus/config/grants, import dry-run and demo protection retained across restart.
- Local picker fixture corrected to actual scoped read grants instead of mistaken user-detail/org-create IDs; no public grants or product seeds changed. Notification unit teardown now awaits outstanding imports, retaining all assertions and unhandled-error checks.
- Final commands, MySQL/browser acceptance, replica/seed/cache migration risks and boundaries: 0.7.1 revalidation.
- Acceptance: 32 backend tests, 202 Vue tests, and 18/18 browser tests passed. SQLite and independent MySQL 8.0.46 both preserve real 0.6.0/0.7.0 fixtures and original Workflow grants across upgrade/restart; fresh database and demo checks passed. Original permission blocker resolved; ready for scoped code/merge review, not production acceptance. Inherited 9 npm advisories remain separately scoped.
- Validation finished before any commit or merge. The user subsequently authorized four scoped conventional commits and fast-forward integration into local
dev; the delivery procedure records the boundary. No push, PR, package publication, production access or deployment is authorized or performed. Customer-change approval remains a separately authorized next task.