feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
This commit is contained in:
@@ -9,12 +9,14 @@ This is internal administration for the independently released HrynCo Notificati
|
||||
- `email_template_definitions`: ID, service ID, key, one shared variables JSON array, revision and timestamps; unique `(service_id, key)`.
|
||||
- `email_templates`: existing language-version ID, definition ID, service ID, service-language ID, subject, HTML body, text body and timestamps; unique `(definition_id, service_language_id)`.
|
||||
|
||||
Composite foreign keys from a version's `(definition_id, service_id)` and `(service_language_id, service_id)` enforce that its language and definition belong to the same service. Service and service-language deletion are restricted by foreign keys; deleting a definition cascades to its versions. The administration does not expose service/definition deletion. Removing a version never removes its definition. Zero-version definitions retain their key, ID and shared variables.
|
||||
Composite foreign keys from a version's `(definition_id, service_id)` and `(service_language_id, service_id)` enforce that its language and definition belong to the same service. Service and service-language deletion are restricted by foreign keys; deleting a definition cascades to its versions. Service and template lists expose Delete links to separate confirmation pages. The owner-authorized REST API has explicit revision-guarded definition/service DELETE operations; service deletion requires all definitions to have been removed first. Removing a version never removes its definition. Zero-version definitions retain their key, ID and shared variables.
|
||||
|
||||
Codes are case-sensitive exact identifiers. `ru`, `ru-RU`, `en` and `EN` remain distinct. Repository and API reads use exact codes without trimming, case folding or locale substitution. The worker first requests the exact code; only after a miss does it retry `LanguageCode.Trim().ToLowerInvariant()` when that string differs ordinally. It never scans case-insensitively, substitutes another region/language or modifies stored codes. New/edited catalog codes allow letters, numbers and hyphens, up to ten characters, with no blank code or empty hyphen segment. This is syntax validation, not a supported-language allowlist. Migration retains existing nonblank codes exactly. Unchanged migrated identifiers such as `en_US` remain valid during service edits; stricter syntax validation applies only to newly added or actually renamed code rows.
|
||||
|
||||
## Administration
|
||||
|
||||
The dark navbar includes **API documentation** at the right, opening `/scalar/v1` in a new tab with an explicit accessible new-tab label and safe opener attributes. All four administration data tables (Services, service templates, Email Channels and embedded Service Languages) explicitly use the shared `admin-data-table` style: contrasting blue headers, outer borders, vertical column separators and vertically centered column headings. Vendor tables are not globally overridden. The template list retains its white filter controls and compact Action column; channel actions and the existing responsive table wrappers remain available. Navbar items and filter controls are vertically centered. When a data-table control receives keyboard focus, only its nearest responsive wrapper's horizontal scroll position is adjusted to expose the full control and focus ring, including newly added language rows. This helper never scrolls the document or vertical ancestors and does not run for pointer-originated focus. Long service names wrap inside list cells and the selected-service heading/breadcrumb rather than widening the page.
|
||||
|
||||
`/admin/services` lists services. Its create/edit form manages Name and an inline table of language codes. Successful creation or editing, including a rename, returns to `/admin/services/{serviceId}` and its template list; validation/save errors retain the service form and entered values. The service page's **Edit Service** action uses the primary blue outline style. There is no separate language editor. Existing code rows retain their IDs when renamed. A transaction-local staging step frees old codes before final assignment so valid code swaps and replacements respect uniqueness without changing version identities. Intermediate codes are never committed or visible to other transactions. Renaming a service or code changes the identifiers used by template API clients and senders; publishers and independently configured email-channel service names must be aligned by the administrator.
|
||||
|
||||
Ordinary removal of an unused language row is saved with the service. For a used language, the form displays its affected version count and blocks ordinary removal. The server repeats this check using the current database count. After a validation or save error, usage counts are reloaded by persisted language ID while preserving entered names/codes and the original revision. If counts cannot be loaded, their display is unavailable and ordinary removal stays disabled.
|
||||
@@ -33,7 +35,11 @@ The language tabs and their Subject/HTML/Text fields form one bordered sheet, wi
|
||||
|
||||
Service and template editor breadcrumbs appear above the editor card, followed by a prominent semantic page heading in its blue header. The shared editor layout keeps breadcrumbs optional so the channel editor continues to work without a breadcrumb section. In the template editor, the selected Service is plain semantic context with a prominent wrapping name beside Template Key; its smaller Edit Service and language codes link remains a separate keyboard-focusable action. Long service names wrap in both the context and template breadcrumb. The hidden service identity and routes are unchanged.
|
||||
|
||||
Preview uses the selected version and sample values from the shared contract. HTML variables are encoded as in the worker, while subject and text stay plain. The iframe is sandboxed without permissions and has a restrictive CSP; scripts, forms, top navigation and external resources are blocked. Save errors preserve entered content and active language; navigation warns about unsaved edits.
|
||||
Preview uses the selected version and sample values from the shared contract. HTML variables are encoded as in the worker, while subject and text stay plain. The owner approved `sandbox="allow-same-origin"` without `allow-scripts` for automatic HTML-preview height on 2026-10-17. This trades the prior opaque origin for trusted parent-side DOM measurement; it does not permit template scripts. The existing restrictive CSP remains unchanged. Scripts, forms, top/pop-up navigation and external resources stay blocked; the trusted parent also prevents preview link navigation and submits. No JavaScript is inserted into the template HTML. On load, content/data-image/width changes and viewport resize, the parent measures content height; resetting height before measurement lets short/empty content shrink. Old ResizeObservers are disconnected when the document changes or page hides. Save errors preserve entered content and active language; navigation warns about unsaved edits.
|
||||
|
||||
The Services and scoped Templates lists offer compact Edit/Delete links. Their separate confirmation pages require antiforgery-protected POST and explicit ConfirmDelete. Definition deletion shows its selected service, key, current translation count and exact codes; it deletes the definition/shared contract/versions while retaining the service/catalog/channels. Service deletion is blocked while any definition exists, including empty definitions, and explicitly retains channels. Revision conflicts reload the current count/revision, clear the checkbox, and require fresh confirmation; definition cancel/success retain the key filter. These routes reuse the revision-protected REST commands and validate selected-service ownership.
|
||||
|
||||
Empty Services uses the existing administration card/empty-state pattern with Create First Service. An empty selected service keeps its table/filter header and offers Create First Template; a filter miss instead offers Clear filter. Template load failures remain errors and never claim an empty catalog. The Email Channels empty-state view is unchanged.
|
||||
|
||||
Every template/service mutation takes the same PostgreSQL transaction advisory lock (`1601`). This makes catalog membership, affected counts and uniqueness checks consistent across API and administration writers. Group and service editors also compare revision tokens, rejecting stale edits rather than overwriting changes. The lock serializes infrequent administration writes; reads and email sends do not take it.
|
||||
|
||||
@@ -56,6 +62,10 @@ Legacy PUT requests have no revision field and retain last-successful-write beha
|
||||
|
||||
The worker still uses the selected language's subject/HTML/text and validates required variables from the shared parent contract. The message envelope, SMTP/channel selection, quotas, retries and ACK/NACK behavior are unchanged. Exact stored versions take priority, so a request for `RU` selects `RU` when both `RU` and `ru` exist. If the exact version is absent, one differing trimmed/lowercase code is tried for compatibility with existing senders (`RU` or ` ru ` can resolve `ru`). A missing `RU-ru` may retry `ru-ru`; it does not search for `ru-RU` or fall back to `ru`. Blank codes are rejected before querying. API reads and writes remain exact, and storage/migration never normalize codes.
|
||||
|
||||
## Whole-definition API for AI clients
|
||||
|
||||
The owner-approved extension provides service read/exact-name lookup and exact supported codes, whole-definition read by stable service ID and key, and one atomic revision-guarded upsert. Shared variables are replaced; supplied translations add/update, omitted translations remain, and an explicit removal list deletes versions without deleting the definition. Validation and merging execute under the same mutation lock as administration/legacy writers. See the [API contract and examples](email-template-definition-api.md) for routes, DTOs, response/error rules, encoding and validation scope. No application authentication or history/restore mechanism is added; the owner's managed-tunnel access decision is recorded without claiming tunnel/Kubernetes verification.
|
||||
|
||||
## Migration and rollout gate
|
||||
|
||||
The current `20261017072410_EmailTemplateLocalizations` EF migration accepts two explicitly recognized source layouts: the flat legacy schema, or the known interim three-entity schema recorded as `20261017070315_EmailTemplateLocalizations`. It checks column names/types/nullability and the expected index/constraint definitions before modifying the schema. An unknown or hybrid layout/history is rejected; the migrator never guesses, drops the database, or rewrites migration history to adopt it. The operation locks the source tables and executes transactionally.
|
||||
@@ -94,3 +104,7 @@ Chromium administration checks on 2026-10-17 included a 1440px desktop viewport
|
||||
|
||||
|
||||
The service-scoped navigation and all-language tab revision was validated separately on 2026-10-17 with temporary host Web at `127.0.0.1:52655` and a fresh, task-owned PostgreSQL database (no restored owner content). Chromium desktop and a 390px touch viewport covered Services name navigation, separate Edit Service, selected-service template list, blank-definition creation, both configured tabs, partial-content errors with no persisted versions, retained input/active tab, keyboard switching, unchanged selected-language preview and HTML isolation/escaping, successful save, and clearing each version through to No versions while keeping the template. The narrow page had no horizontal overflow. The temporary Web and browser tab were stopped afterward. This is browser validation of the Notification Service administration, not validation of an installed Invemory Android application.
|
||||
|
||||
The later administration styling pass checked all four `admin-data-table` views at 1440, 390 and 320 pixels using temporary Web `127.0.0.1:52656` and task-owned PostgreSQL `58519`. Shared header colors/borders/vertical alignment, long service-name wrapping, filter-miss headers, compact actions, and table-local scrolling passed without page overflow. Keyboard focus exposed the complete channel/template/language actions and newly added language rows. The navbar remained 52px tall with matching brand/link centers, and API documentation opened a rendered Scalar page in a separate tab with a null opener; link contrast was 14.63:1. The full solution build and 148 tests passed (32 service/worker and 116 integration tests). Temporary resources were removed afterward. Remove behavior and the separate DeleteLanguage confirmation page were retained; no modal UX was introduced.
|
||||
|
||||
The final preview/deletion browser pass used the same task-owned host and database. At 1440px, long content grew to 1047px and an empty language tab shrank to 80px; at 390px the corresponding measurements were 997px and 80px. At 320px responsive text/data-image content grew from 1206px to 1653px when the image height changed, with document width remaining 320px. Preview script execution stayed blocked, and parent-side event checks cancelled top/self links and form submission while retaining about:srcdoc. At 320px the definition confirmation rejected an unchecked submission, then confirmed deletion returned to the scoped empty-template state; nonempty service deletion was blocked, and confirmed empty-service deletion reached the empty Services state. The table header/filter and first-create links remained visible with no page overflow. A separate read-only owner-Web browser check confirmed Invemory-Prod and its eight imported definitions; no owner data was changed.
|
||||
|
||||
Reference in New Issue
Block a user