Batch 8 shipped UI-layer blurring: when a user lacked access, the shell visually masked the page. Batch 12 adds a server-issued gating envelope at GET /api/access/gate?doc_id=… that returns: render_mode (full/restricted/blocked), allow_read/share/export, bilingual banner, and an optional stub_html for blocked pages. The shell, on shelled pages, fetches this envelope and replaces document.body with the stub when render_mode=blocked.
Server returns the stub; shell displays the stub. No more guessing at the client. Still UI-layer: the server does NOT intercept HTML delivery for legacy pages.
Server คืน stub · shell แสดง stub · ไม่ต้องเดาฝั่ง client · แต่ยังเป็นระดับ UI · legacy pages ไม่ถูก intercept ที่ระดับ HTTP
Gating runs entirely at the Document Access Service boundary. Shelled pages call /api/access/gate?include_stub=true on DOMContentLoaded. When render_mode=blocked, the shell swaps body HTML. When restricted, the shell keeps content but shows the banner + disables share/export. When full, nothing changes. Legacy pages that do not call the endpoint see no gating — documented honestly.
GET /api/access/gate?doc_id=/planning/document-access-model.html&include_stub=true
Cookie: ds_session=...
=>
{
"doc_id": "/planning/document-access-model.html",
"group_id": "planning",
"state": "restricted",
"allow_render": true,
"render_mode": "restricted",
"allow_read": true,
"allow_share": false,
"allow_export": false,
"reason_en": "Content restricted under your current access profile.",
"reason_th": "เนื้อหาถูกจำกัดภายใต้โปรไฟล์ปัจจุบัน",
"stub_html": null,
"profile_id": "u-external-001",
"email": "partner@external.example",
"mode": "local-dev",
"resolved_at": "2026-04-20T10:45:00Z",
"honest_banner": { "en": "...", "th": "..." }
}| visibility state | render_mode | stub_html when include_stub=true | client behaviour |
|---|---|---|---|
| visible | full | null | no change |
| restricted | restricted | null | banner + disable share/export |
| hidden-doc | blocked | stub html returned | replace body with stub |
| hidden-group | blocked | stub html returned | replace body with stub |
| not-granted | blocked | stub html returned | replace body with stub |
Same as Batch 8 — allow_share and allow_export drive button disable/enable. Batch 12 hardens by:
Enforced (shell calls /api/access/gate on load):
Not enforced: