23-P3D4C2U — Table Module Read-Only Exposure Design Note
23-P3D4C2U — Table Module Read-Only Exposure Design Note
Date: 2026-05-08 Status: DESIGN (read-only inventory + design proposal). No implementation. Scope: Expose universal event core (
event_outbox+ read-state) through the existing Table Module / DirectusTable shelf — without a bespoke notification UI. Result: VIABLE viaPG_VIEW_PROJECTION+table_registryrow + DOT-driven Directus permission seed. Nuxt path =NO_NUXT_CODE.
1. Anchor: the Assembly path
Canonical pipeline (Điều 7 / Assembly Module SSOT / Table Module SSOT v4.1):
PG schema (or PG view)
→ Directus collection / metadata / permissions
→ table_registry row (id, table_id, collection, fields, sort, filter, page_url, flags)
→ DirectusTable.vue runtime (GET table_registry → GET items)
→ existing Nuxt route /knowledge/registries/{singular}
Hard rule (Table Module SSOT v4.1):
"Mỗi bảng trong UI = 1 record trong
table_registry(Directus). Không có record = bảng đó KHÔNG ĐƯỢC PHÉP TỒN TẠI."
This pack must add ONE row, not a new shelf.
2. Live inventory (verified 2026-05-08)
2.1 table_registry — 20 active rows, generic-route convention works
Verified via Directus GET /items/table_registry:
| Signal | Evidence |
|---|---|
| Total rows | 20 (ids 1..20) |
| Generic route convention | /knowledge/registries/{singular} — used by 13/20 rows incl. system_issues, agent, dot_tool, module, page, collection, checkpoint_*, table |
tbl_system_issues precedent |
✅ id=20, collection=system_issues, page_url=/knowledge/registries/system_issue, default_sort=-detected_at, status=published, rows_per_page=50, enable_search=true |
| Field-config shape | fields: [{key,label,sortable?,filterable?,filterOptions?}...]; settings: default_sort, default_filter (Directus filter object), rows_per_page, enable_search, enable_pagination, enable_insert_marks, enable_proposals, module, status |
| Module status (registry SSOT) | tbl_system_issues published — proves shelf accepts an event-domain, append-only, timeseries-style table |
2.2 Directus collection visibility
directus_list_collections returns event_* tables already auto-discovered as whitelisted Directus collections:
event_outboxevent_pendingevent_readevent_subscriptionevent_type_registry
Read attempt (GET /items/event_outbox) → HTTP 403 for the admin token currently in use. Interpretation: the collections are introspected but no read role/permission is seeded yet. This is the gap P3D4C2U must close via DOT, not by click-config.
2.3 PG event core — live deviations to preserve
From P3D4C1U report (PASS, deviations carried forward):
event_subject_ref = text (D1; system_issues.id is integer)
read_status_source IN ('explicit','implicit_self') (D2)
event_pending = empty (immediate-only PoC, seam preserved)
worker_created = false
pg_cron_used = false
fn_event_unread EXISTS
fn_event_mark_read EXISTS
fn_event_board NOT created
Functions are NOT in scope for Table Module rendering (function-backed display = FUNCTION_BACKED_NOT_ALLOWED_NOW). DirectusTable consumes a collection or view, not a function.
2.4 No projection view exists yet
KB search (v_event_outbox*, v_event_*) returned only design sketches (P3D4B v_iu_notification_board non-executable, P3D4C0X 5-layer note). No live v_event_outbox_table in PG today.
3. Display source decision — PG_VIEW_PROJECTION
Options evaluated:
| Option | Verdict | Rationale |
|---|---|---|
RAW_EVENT_OUTBOX |
❌ rejected | event_outbox carries payload jsonb + payload_classification + potential body/raw refs. Directus field-allowlist alone is brittle (any future column auto-leaks unless registry is updated). Schema-level masking is safer + simpler. |
PG_VIEW_PROJECTION |
✅ chosen | One read-only view exposes only the metadata allowlist; payload/body never reach Directus. Future read-state join (LEFT JOIN event_read for is_read flag) lives in the view, not in DirectusTable config. Aligns with Điều 43 "consolidated read-only VIEW, 0 duplicate, 0 drift" pattern. |
FUNCTION_BACKED_NOT_ALLOWED_NOW |
n/a | DirectusTable cannot consume fn_event_unread. Defer. |
BLOCKED |
n/a | Path is viable. |
3.1 Candidate view — non-executable sketch
⚠️ Sketch only; column types, ordering, and Directus introspection compatibility must be verified at the implementation prompt. Do NOT copy-paste-execute.
view name : v_event_outbox_table
purpose : metadata-only read projection of event_outbox for Table Module
columns : event_id (uuid) -- PK proxy
occurred_at (timestamptz) -- default sort key
event_domain (text)
event_type (text)
event_stream (text)
delivery_lane (text) -- 'immediate' | 'delayed'
severity (text)
event_subject_table (text)
event_subject_ref (text) -- preserve D1: text not uuid
canonical_address (text)
actor_ref (text)
source_system (text)
payload_classification (text)
-- joined later (Phase optional):
-- is_read (bool) via LEFT JOIN event_read ON event_id+actor
-- read_status_source (text) — propagate D2 values verbatim
ordering : occurred_at DESC, event_id DESC (deterministic tie-break)
NOT exposed:
- payload jsonb / body / raw
- vector / embedding fields if present
- secret / token / password
- any unrestricted JSON
Ownership: PG view owned by Directus PG role (whatever system_issues view-style collections use); read GRANT only.
4. Draft table_registry declaration
Modeled on row #20 (tbl_system_issues):
{
"table_id": "tbl_event_outbox",
"name": "Sổ sự kiện hệ thống (Event Outbox)",
"collection": "v_event_outbox_table",
"page_url": "/knowledge/registries/event_outbox",
"status": "draft",
"module": null,
"default_sort": "-occurred_at",
"default_filter": null,
"enable_search": true,
"enable_pagination": true,
"enable_insert_marks": false,
"enable_proposals": false,
"rows_per_page": 50,
"fields": [
{"key": "occurred_at", "label": "Thời điểm", "sortable": true, "filterable": true},
{"key": "event_domain", "label": "Miền", "sortable": true, "filterable": true},
{"key": "event_type", "label": "Loại sự kiện", "sortable": true, "filterable": true},
{"key": "event_stream", "label": "Luồng", "sortable": true, "filterable": true},
{"key": "delivery_lane", "label": "Kênh phát", "sortable": true, "filterable": true,
"filterOptions": ["immediate","delayed"]},
{"key": "severity", "label": "Mức độ", "sortable": true, "filterable": true},
{"key": "event_subject_table", "label": "Bảng đối tượng", "sortable": true, "filterable": true},
{"key": "event_subject_ref", "label": "Khóa đối tượng", "sortable": false, "filterable": true},
{"key": "canonical_address", "label": "Địa chỉ chuẩn", "sortable": false, "filterable": false},
{"key": "actor_ref", "label": "Tác nhân", "sortable": true, "filterable": true},
{"key": "source_system", "label": "Hệ nguồn", "sortable": true, "filterable": true},
{"key": "payload_classification", "label": "Phân loại payload","sortable": true, "filterable": true}
]
}
Notes:
enable_insert_marks=false,enable_proposals=false: read-only consumption, no write affordance.mark_read_ui= DEFERRED (out of P3D4C2U; would require write permission + bespoke UI which is forbidden).- Status
draftuntil DOT registration prompt approved; flip topublishedat execution.
5. Minimal DOT-driven Directus exposure plan
DOT-only, no click-config. Suggested handler split (to be drafted in next pack):
| Step | Operation | Object | Idempotent? |
|---|---|---|---|
| 1 | Create PG view v_event_outbox_table |
PG | yes (CREATE OR REPLACE VIEW) |
| 2 | GRANT SELECT on view to Directus PG role | PG | yes |
| 3 | Directus collection meta seed (note, display_template, archive_field=null, hidden=false) |
directus_collections |
yes (DOT-managed seed) |
| 4 | Directus field meta seed for the 12 visible columns (interface, display) | directus_fields |
yes |
| 5 | Directus permission seed: role=public_readonly (or existing agent_readonly) → action=read, fields=allowlist, no filter |
directus_permissions |
yes |
| 6 | Insert table_registry row (status=draft first, flip to published after smoke) |
table_registry |
yes (filter by table_id=tbl_event_outbox) |
| 7 | Smoke: GET /items/v_event_outbox_table?limit=1 returns 200; GET /items/table_registry?filter[table_id][_eq]=tbl_event_outbox returns 1 row |
— | — |
| 8 | Rollback path: revoke permission → drop registry row → drop view | — | — |
Verify pair (Điều 33 APR / DOT convention): the registration handler must ship with a paired verify handler that reproduces step 7 and asserts schema diffs match expectations.
No raw INSERT into Directus tables that bypass cache invalidation: prefer Directus REST/Items API; if raw INSERT INTO directus_* is necessary (per S178 Fix 20 M3B precedent), restart Directus after.
6. Nuxt path — NO_NUXT_CODE
Justification:
tbl_system_issues(id=20) already uses/knowledge/registries/system_issue. The Nuxt repo carries a generic registry route that mounts<DirectusTable tableId="...">; adding a sibling URL/knowledge/registries/event_outboxwith the same pattern requires no new component.- DirectusTable is TPL-001 (Điều 28). Adding
tbl_event_outbox= a new TPL-001 instance via config declaration. NO new template, NO fork. - Caveat (inventory uncertainty): Table Module SSOT v4.1 still lists TT-020/021/022 as 🔴 (DirectusTable wiring to registry "đang xây"). However, 13 of 20 registry rows are already
publishedand follow the/knowledge/registries/{singular}pattern, indicating the route convention is in production. The implementation prompt should run a smoke check on/knowledge/registries/system_issuerendering before flippingtbl_event_outboxtopublished. If the generic route is not actually wired, the recommendation flips toNEEDS_GENERIC_TEMPLATE_EXTENSION.
Forbidden (re-stated for the implementer):
- bespoke notification page or component
- notification-specific business logic in Nuxt
- Nuxt direct PG
- Directus mark-read write endpoint in this pack
- modification of Điều 43 context-pack builder
7. Điều 43 / graphic alignment
Điều 43 is referenced as model + context source, not modified.
Pattern reuse:
- Điều 43
context_pack_section_definitionsproves "config-driven row + generic dispatcher = zero-code add" — same shape astable_registry. - The proposed view
v_event_outbox_tablecould later become a data source for an Điều 43 section (e.g. anEVENT_RECENTsection pointing to the same view). That is out of scope here; no section_definition is added in this pack. architecture_mmdalready represents Gate/Warehouse/Brain relationships. Event-flow visualization, if needed later, must be added as a new Điều 43 section row, not a parallel diagram system.
Hard boundary respected: P3D4C2U does NOT touch context-pack builder, verify, or section_definitions.
8. Risk register
| Risk | Likelihood | Mitigation |
|---|---|---|
event_outbox schema evolves and adds an unsafe column (e.g. body) |
medium | View has explicit allowlist of columns; new columns in base table do NOT auto-flow to view. Re-deploy view to expose. |
| Directus introspection mishandles a PG view (no PK) | low-medium | Set directus_collections.singleton=false + use event_id as virtual PK in collection meta seed. Verify in smoke. |
| Generic Nuxt route not actually wired (TT-020/021 🔴) | low | Smoke system_issue registry page before publishing event_outbox. If broken, downgrade recommendation to NEEDS_GENERIC_TEMPLATE_EXTENSION. |
event_subject_ref rendered as link/lookup confuses users (text not FK) |
low | Field config keeps it text-only; no relational interface. Document in field meta note. |
| Permission seed accidentally grants write | medium | Permission row scoped to action='read'; verify pair must assert no create/update/delete rows exist for this collection. |
9. Decision summary
display_source = PG_VIEW_PROJECTION
pg_projection_needed = YES
pg_projection_name = v_event_outbox_table
table_registry_declaration = drafted (§4)
directus_dot_plan = drafted (§5, 8 steps + verify pair + rollback)
nuxt_path = NO_NUXT_CODE (caveat: smoke generic route first)
mark_read_ui = DEFERRED
recommendation = PROCEED_TO_DOT_TABLE_REGISTRATION_PROMPT
next_required_pack = P3D4C2U_DOT_TABLE_REGISTRATION_IMPLEMENTATION_PROMPT_REVIEW
Strongest output (per prompt §9): universal events can be exposed by adding one read-only PG view + one table_registry row + DOT-managed Directus permission seed. No new shelf. No bespoke UI.