KB-793C

23-P3D4C2U — Table Module Read-Only Exposure Design Note

14 min read Revision 1
p3d4c2udesigntable-moduledirectusnuxtreadonlyevent-outboxassembly-first

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 via PG_VIEW_PROJECTION + table_registry row + 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_outbox
  • event_pending
  • event_read
  • event_subscription
  • event_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 draft until DOT registration prompt approved; flip to published at 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_outbox with 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 published and 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_issue rendering before flipping tbl_event_outbox to published. If the generic route is not actually wired, the recommendation flips to NEEDS_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_definitions proves "config-driven row + generic dispatcher = zero-code add" — same shape as table_registry.
  • The proposed view v_event_outbox_table could later become a data source for an Điều 43 section (e.g. an EVENT_RECENT section pointing to the same view). That is out of scope here; no section_definition is added in this pack.
  • architecture_mmd already 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.

Back to Knowledge Hub knowledge/dev/laws/dieu44-trien-khai/design/23-p3d4c2u-table-module-readonly-exposure-design.md