feat(push): add service-owned firebase channels and delivery
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation. Ref: IT-1074
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
# Push channels and Android FCM — IT-1074
|
||||
|
||||
Notification Service owns Firebase configuration. Invemory owns installation/user bindings
|
||||
and notification preferences; it publishes a push request without Firebase credentials or OAuth access tokens.
|
||||
The public Contracts package must be delivered first; updating Invemory is a separate step.
|
||||
|
||||
## Administration
|
||||
|
||||
The **Push Channels** menu opens /admin/push-channels. Create/Edit use the existing service
|
||||
catalog (ServiceId UUID), Firebase Project ID, service account JSON and Active. Fields use
|
||||
compact desktop widths and wrap on small screens. A service has one push channel, enforced
|
||||
by a unique ServiceId index. There is no priority/fallback or extra routing configuration.
|
||||
Inactive channels are retained but never selected for sending. Reassignment is allowed;
|
||||
service rename automatically updates the displayed name without breaking the FK.
|
||||
|
||||
Creating a channel requires a valid service-account JSON whose project_id matches the
|
||||
Firebase Project ID. The server validates its type, email and RSA private key without
|
||||
contacting Google. Editing with an empty credentials field preserves the stored value;
|
||||
changing the project requires replacement credentials. An inactive channel also requires
|
||||
valid credentials, so activation does not introduce a partly configured channel.
|
||||
|
||||
Safe list/edit models contain no secret. Edit does not load credentials into the browser,
|
||||
and validation failures clear both the model and attempted ModelState value before rendering.
|
||||
Credential parser/provider errors use fixed messages and never log the JSON/private key.
|
||||
The Worker constructs Google credentials from the validated service-account email/private
|
||||
key; supplied token_uri or other endpoint fields are not used. There is no ADC fallback.
|
||||
|
||||
Storage follows the existing SMTP approach: credentials are server-only JSONB in the
|
||||
Notification Service database, without new application-level encryption. Restrict access
|
||||
to this private administration app, its database and backups. Existing private-network
|
||||
access assumptions are unchanged; this feature adds no public administration endpoint.
|
||||
|
||||
## Persistence and migration
|
||||
|
||||
Apply 20261024080457_AddPushChannels before opening Push Channels or enabling delivery.
|
||||
The additive migration creates push_channels with a required services.id FK using RESTRICT
|
||||
and unique service_id index. It does not alter existing email channels, settings or usage.
|
||||
Service deletion is blocked while either email or push channels still reference it; reassign
|
||||
a push channel before deleting its service. Saves reuse the service administration transaction
|
||||
lock to serialize service membership and duplicate-channel checks. Migration downgrade drops
|
||||
push channels and their credentials, so it is destructive after channels have been configured.
|
||||
|
||||
The existing DAL.EF/update-development-database.bat applies the schema to the database from
|
||||
the Web Development configuration; it does not provision containers or restart Web/Worker.
|
||||
|
||||
## Worker and contracts
|
||||
|
||||
Set FcmPush__Enabled=true to register the push consumer. Firebase project/credentials and
|
||||
service routing come from the active channel, not environment settings or producer payloads.
|
||||
The previous draft FcmPush__ServiceName host filter is removed: each request's exact current
|
||||
ServiceName resolves to services.id and then that service's active push channel. Missing
|
||||
services, missing/inactive channels and invalid stored configuration produce safe terminal
|
||||
failure results without contacting Firebase. No legacy service-name alias is used at runtime.
|
||||
|
||||
SendPushMessageData retains registration/binding/user identity, ServiceName, device Token
|
||||
and TokenHash, title/body, version, /home route and ExpiresAt. FcmProjectId, FcmAccessToken and
|
||||
FcmAccessTokenExpiresAt have been removed. SupportedMessageType remains public in Contracts;
|
||||
the existing Notification.SendPush.v1 header and notification.send-push queue are unchanged.
|
||||
OAuth tokens are obtained by the Firebase SDK inside Notification Service from the channel's
|
||||
service account. They are never included in RabbitMQ request/result contracts.
|
||||
|
||||
This replaces the unpublished draft that obtained OAuth authorization in Invemory. Source
|
||||
clients still initializing the removed properties must update after the new Contracts package
|
||||
is explicitly published. Old payloads containing additional JSON fields are not a configuration
|
||||
source; the server ignores those fields and still requires its own active channel. Do not
|
||||
resume producers that send Firebase secrets until that separate Invemory update removes them.
|
||||
No Invemory source or production configuration is changed by this task.
|
||||
|
||||
The Worker sends bounded version-1 data-only messages with /home routes and a maximum
|
||||
15-minute expiry, preserving Android binding checks before display. Delivery outcomes are
|
||||
persisted by service/message ID under an advisory lock and replayed after reply failures.
|
||||
A crash after FCM acceptance but before DB commit may still repeat delivery; this is not
|
||||
exactly-once delivery. Expired messages are not sent.
|
||||
|
||||
UNREGISTERED invalidates only the matching registration/binding/token-hash. Sender mismatch,
|
||||
invalid argument and authorization/configuration failures do not invalidate device tokens.
|
||||
Transient delivery uses the existing three attempts, exponential delay, jitter and Retry-After.
|
||||
Outcomes expire after 30 days; existing outcome/retry metrics contain no target/token labels.
|
||||
Firebase acceptance does not guarantee Android display (force-stop, Doze and connectivity apply).
|
||||
|
||||
## Validation boundaries
|
||||
|
||||
Tests cover the administration lifecycle, secret non-disclosure, unchanged-secret edits,
|
||||
service reassignment/rename, inactive selection, duplicate/missing services, project changes,
|
||||
worker name-to-ID resolution, server credential selection and credential-free Contracts.
|
||||
All provider routing tests use a fake Firebase client. Real Firebase delivery, credential
|
||||
permissions and Android notification display require a separate end-to-end verification
|
||||
with an authorized configured project/device; fake local credentials cannot prove those.
|
||||
|
||||
On 2026-10-24 the Release solution build and targeted tests passed (21 unit and
|
||||
24 integration). Rider inspection reported no errors in the changed production classes;
|
||||
the existing Firebase SDK Token deprecation warning remains. Structural review kept
|
||||
changed methods within cognitive complexity 10; the tool did not report numeric scores.
|
||||
The review compared email-channel mapping/editor patterns, service catalog controllers,
|
||||
transaction locking and existing push delivery/retry behavior. No unresolved blocking
|
||||
findings remained for the implemented scope.
|
||||
|
||||
Desktop browser validation used a disposable database and Web at 127.0.0.1:52658, with
|
||||
an offline-generated test service account (no Google account or network send): empty
|
||||
list, create, invalid JSON feedback/secret clearing, edit without secret disclosure, and
|
||||
Active off with credentials preserved all passed. Mobile wrapping was inspected in CSS
|
||||
but not exercised on an Android device. The temporary Web and database container were
|
||||
removed after validation. The owner-applied AddPushChannels migration was separately
|
||||
confirmed in the existing development database; it was not reapplied.
|
||||
|
||||
## Channel tests — 2026-10-24
|
||||
|
||||
The editor has separate **Check Firebase authorization** and **Send test push** actions,
|
||||
following the current-form SMTP test workflow. Neither action saves a channel. New channels
|
||||
use the entered service-account JSON; blank JSON on an existing channel uses its stored
|
||||
credentials only if the project is unchanged. Inactive channels can be tested explicitly.
|
||||
Both POST endpoints require the form antiforgery token and bounded request bodies.
|
||||
The existing private-network/port-forward access boundary is retained: this Web application
|
||||
has no in-app authentication or administrator-role middleware. Do not expose it publicly.
|
||||
|
||||
Authorization requests an OAuth access token with the firebase.messaging scope and a
|
||||
30-second timeout. It neither requires a recipient nor calls FCM send. A successful OAuth
|
||||
exchange proves service-account authentication only, not FCM API enablement, send permission
|
||||
or device delivery. The access token is never returned, saved or logged.
|
||||
|
||||
Send opens a separate Recipient token field and sends a neutral notification message through
|
||||
the same FirebasePushClient transport using current form credentials. RecipientToken is not
|
||||
part of the save model or channel storage. The dialog clears it after the attempt or close.
|
||||
Only test on an explicitly supplied, owned test-device token. Provider acceptance is shown
|
||||
as acceptance, never as verified delivery/display. This direct provider diagnostic does not
|
||||
exercise Invemory's data-only binding, expiry/tap checks, transactional Outbox or result queue;
|
||||
it does not replace that separate end-to-end validation. Foreground display depends on the
|
||||
receiving app. No user inventory or account data is included.
|
||||
|
||||
Errors are fixed, safe messages; no provider exception, credentials, access token or recipient
|
||||
is echoed. The UI uses textContent for results. Saved credentials never populate the editor.
|
||||
|
||||
### Channel-test verification
|
||||
|
||||
Release solution build passed. 29 focused push unit tests and 9 PushChannelAdmin integration
|
||||
tests passed, including CSRF rejection, missing credentials, unchanged/changed project reuse,
|
||||
unsaved credentials, separate authorization/send calls, transient errors and no persistence.
|
||||
Rider's final changed-file error inspection was clean. Existing FirebaseAdmin Message.Token
|
||||
obsolescence and Microsoft.OpenApi vulnerability warnings remain outside this task.
|
||||
Cognitive complexity was manually reviewed, not numerically measured.
|
||||
|
||||
A temporary local Web instance on 127.0.0.1:52658 verified both buttons, modal recipient input,
|
||||
empty-token validation, missing-credential response and clearing the recipient after a failed
|
||||
attempt. Only a fake non-deliverable token with missing credentials was submitted; the
|
||||
request stopped before Firebase. No channel was created or modified by this UI verification.
|
||||
Code review compared current SMTP test behavior, existing credential parser/provider,
|
||||
Razor editor layout, antiforgery and repository interfaces. Fixed script initialization to
|
||||
wait until footer buttons exist and rejected malformed/oversized test requests safely.
|
||||
The final review found no further actionable issue in these two actions.
|
||||
|
||||
The owner subsequently configured the local channel, verified OAuth authorization and sent
|
||||
a real test to the current Samsung token on 2026-10-24. The owner confirmed notification
|
||||
display and opening the installed application on tap. This validates the direct Firebase
|
||||
notification test, not the Invemory data-only/outbox flow. The installed receiver intentionally
|
||||
validates data-only binding fields; a notification-only test received while Invemory is in
|
||||
the foreground does not satisfy that contract. Background notification display is handled
|
||||
by the Firebase SDK. No additional diagnostic messages were sent by the agent.
|
||||
|
||||
The authorization button and its result are directly beneath the service-account JSON
|
||||
field. The recipient input and Send test push button remain in the separate test dialog.
|
||||
|
||||
Delivery verification: the full Release solution build and all 221 tests passed (67 unit,
|
||||
154 integration). The pre-existing Microsoft.OpenApi 2.0.0 vulnerability warning remains.
|
||||
Delivery is restricted to Notification Service; Invemory producer/client migration and its
|
||||
runtime validation remain separate pending work. TeamCity main triggers include general
|
||||
checks, image publication, Contracts publication and production deployment. The owner
|
||||
explicitly authorized this automatic production consequence for the Notification Service
|
||||
delivery on 2026-10-24. It does not authorize delivery of the remaining Invemory repositories.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Email channels: stable service association
|
||||
|
||||
Email channels reference `services.id` (UUID) through required `email_channels.service_id`.
|
||||
The `(service_id, priority)` index supports channel selection; the service FK uses RESTRICT,
|
||||
so service deletion cannot remove channels or their usage. Service names in channel reads
|
||||
come from the related service, not a copied string. Create/Edit select an existing service;
|
||||
Edit can reassign a channel without changing its identity or statistics.
|
||||
|
||||
## Migration
|
||||
|
||||
`LinkEmailChannelsToServices` validates every legacy channel name before replacing the
|
||||
string column. An exact, case-sensitive service-name match is accepted. The owner-approved
|
||||
legacy mapping `StoreMate-Prod` → `Invemory-Prod` is also accepted; the existing staging
|
||||
name `Invemory.Staging.Sandbox` uses an exact match. Every channel must have exactly one
|
||||
candidate service. Missing names or ambiguity (for example both legacy and renamed
|
||||
services existing) abort the migration. No service is guessed, created, renamed or deleted.
|
||||
|
||||
The migration runs in an EF transaction and locks services/channels while checking and
|
||||
backfilling. Failure rolls back schema and data. Channel IDs, SMTP settings (including
|
||||
secrets and sender settings), priority, quotas, warning threshold, active state, timestamps
|
||||
and usage records remain unchanged. No SMTP settings are logged by the migration.
|
||||
Downgrade reconstructs the current service names; historical names are not restored.
|
||||
|
||||
Before a separately authorized rollout, audit unmatched/ambiguous names and resolve them
|
||||
with the owner, take the normal backup, and coordinate Web/Worker downtime with migration:
|
||||
old binaries use the removed `service_name` column. Administrative POST/PUT clients must
|
||||
send `serviceId`; name-only writes are no longer supported. RabbitMQ producers retain
|
||||
their existing `ServiceName` wire field and must send the current service name. The
|
||||
approved legacy mapping applies only during migration, never as a runtime fallback.
|
||||
|
||||
## Verification
|
||||
|
||||
### Local database update on Windows
|
||||
|
||||
Run from any directory, without arguments or environment setup:
|
||||
|
||||
```bat
|
||||
C:\projects\HrynCo\hrynco-notification-service\HrynCo.NotificationService.DAL.EF\update-development-database.bat
|
||||
```
|
||||
|
||||
The script resolves its directory through %~dp0, reads the existing Web
|
||||
`appsettings.Development.json`, builds the EF project in Release and applies pending
|
||||
migrations. It requires dotnet-ef and Windows PowerShell. It validates a loopback host and a nonempty database name before invoking EF.
|
||||
The port and database come from the Development configuration without hardcoded overrides.
|
||||
A remote host is rejected; this script is specifically for the local development database. It never installs, recreates or restarts containers.
|
||||
|
||||
The connection is passed only through the child process's `App__ConnectionString`;
|
||||
the design-time factory reads it. Credentials are neither echoed nor passed as CLI
|
||||
arguments, and inherited connection overrides are replaced with the verified Development
|
||||
configuration. The script returns EF's exit code (or a nonzero code for configuration
|
||||
failure). Restart local Web/Worker after a schema change to use matching binaries.
|
||||
|
||||
### Tests
|
||||
|
||||
PostgreSQL integration tests exercise exact and approved-alias backfill, preserved channel
|
||||
settings/limits/usage, downgrade, missing/ambiguous-name rollback, FK enforcement, service
|
||||
rename, API validation, editable reassignment with a blank password, and service deletion
|
||||
after reassignment. Worker tests verify name-to-ID channel resolution and sending/usage.
|
||||
All migration rehearsals use disposable local databases; they do not alter production.
|
||||
|
||||
On 2026-10-24, the Release solution build, 54 service/worker tests and 145 integration
|
||||
tests passed. A follow-up check explicitly verified both invalid update IDs reach the
|
||||
service-existence validator (2/2 passed). Rider inspected the changed production methods;
|
||||
remaining findings were existing/style suggestions. Cognitive complexity was reviewed
|
||||
structurally against the limit of 10; no numeric score was produced by the inspection.
|
||||
|
||||
After separate owner authorization, the existing local container database was backed up
|
||||
and updated with the already-built Migrator. Its two pending migrations were
|
||||
`20261023223753_AddPushDeliveries` and `20261024073202_LinkEmailChannelsToServices`.
|
||||
No email channels or usage records existed in this local database before the update;
|
||||
their unchanged empty state and the installed RESTRICT FK were verified afterwards.
|
||||
The development batch script was then executed from its own folder and another working
|
||||
directory: both returned 0 and reported no pending migrations. An isolated fake EF
|
||||
failure verified exit-code propagation (37). The current local Web at port 5188 rendered
|
||||
the dropdown with the existing service catalog. Disposable test containers were removed.
|
||||
Production was not accessed or migrated.
|
||||
|
||||
The existing private-network administration access model is unchanged. This change does
|
||||
not configure Firebase or resume the paused push-channel architecture.
|
||||
@@ -21,7 +21,7 @@ A variable is `{ "name": "Name", "required": true }`. A translation contains `id
|
||||
|
||||
`POST /api/v1/services` accepts `{ "name": "ExampleService", "languages": [{ "code": "en" }, { "code": "uk" }] }`. It returns 201 with a Location to the service read and the complete service DTO. Names are exact and unique. The required languages array may be empty; new services cannot reference persisted language IDs.
|
||||
|
||||
`PUT /api/v1/services/{serviceId}` accepts the last-read nonempty `revision`, `name`, and a required full `languages` array. Retain each existing language's ID from the read DTO; omit/empty the ID for a new code. The returned service DTO includes the new revision and generated language IDs. Omitted unused catalog rows are removed; omitting a used language returns 409 with no changes. A stale revision returns 409; a missing service returns 404. Existing migrated nonstandard codes remain valid unchanged, and the existing stricter syntax validation applies only to added/renamed codes. Renames do not rewrite independently configured channel service names.
|
||||
`PUT /api/v1/services/{serviceId}` accepts the last-read nonempty `revision`, `name`, and a required full `languages` array. Retain each existing language's ID from the read DTO; omit/empty the ID for a new code. The returned service DTO includes the new revision and generated language IDs. Omitted unused catalog rows are removed; omitting a used language returns 409 with no changes. A stale revision returns 409; a missing service returns 404. Existing migrated nonstandard codes remain valid unchanged, and the existing stricter syntax validation applies only to added/renamed codes. Email channels reference the stable service ID; their displayed service name updates immediately after a rename. Publishers must use the current name.
|
||||
|
||||
`DELETE /api/v1/services/{serviceId}/languages/{languageId}?revision={serviceRevision}` removes an unused catalog row only. Used languages return 409; use the explicit destructive endpoint when the versions should be deleted:
|
||||
|
||||
@@ -40,7 +40,7 @@ All three body properties are required. The nonnegative count and service revisi
|
||||
- `DELETE /api/v1/services/{serviceId}/email-template-definitions?key={encodedKey}&revision={lastReadDefinitionRevision}` removes the selected definition and all of its versions atomically.
|
||||
- `DELETE /api/v1/services/{serviceId}?revision={lastReadServiceRevision}` requires **every definition** to have been removed first, including zero-version definitions; otherwise it returns 409. It removes the remaining catalog languages and service atomically.
|
||||
|
||||
Both endpoints return HTTP 200 with the existing `ApiResponse<Unit>` success envelope (`data: {}`). A missing resource returns 404, missing/empty/malformed revision or invalid key returns 400, and stale revision returns 409. Every check and mutation runs under the common transaction advisory lock. Concurrent definition creation cannot race past the service's nonempty check. These operations do not remove email channels, whose service name is a separate identifier without a service foreign key.
|
||||
Both endpoints return HTTP 200 with the existing `ApiResponse<Unit>` success envelope (`data: {}`). A missing resource returns 404, missing/empty/malformed revision or invalid key returns 400, and stale revision returns 409. Every check and mutation runs under the common transaction advisory lock. Concurrent definition creation cannot race past the service's nonempty check. These operations do not remove email channels. A service with linked channels returns 409; reassign or explicitly remove its channels before deleting it. The foreign key restricts deletion.
|
||||
|
||||
## Atomic upsert
|
||||
|
||||
@@ -77,10 +77,12 @@ In the admin channel editor, **Send Test Email** tests the form's current values
|
||||
|
||||
Saving an existing channel follows the same password rule: a blank password keeps the saved value when host, port, username, and SSL/TLS mode are unchanged, so sender name/address and limit edits do not require re-entering it. A changed SMTP connection needs an explicitly entered password. Creating a channel in the admin editor still requires a password. Validation errors redisplay the editor without rendering the saved or submitted password.
|
||||
|
||||
- `GET /api/v1/email-channels/all` enumerates **all** channels, including channels whose exact `serviceName` has no service row.
|
||||
- `GET /api/v1/email-channels/all` enumerates all channels, each linked to an existing service by its UUID `serviceId`. Read DTOs include `serviceId` and the current `serviceName`.
|
||||
- Existing `GET /api/v1/email-channels?serviceName={exactName}`, GET by ID, POST, PUT by ID and DELETE by ID remain available. Create returns 201/Location and its existing GUID response; update/delete return 200/Unit.
|
||||
- Create supports SMTP (`channelType: 1`). Request/response `settings` explicitly maps `host`, `port`, `username`, `password`, `useSsl`, `fromEmail`, and `fromName`; reads no longer lose derived SMTP fields through an abstract JSON type. Host/fromEmail and a valid port are required. The optional empty username/password support unauthenticated development SMTP.
|
||||
- Existing PUT behavior is retained: it changes settings, priority, limits, warning threshold and active state, **not** serviceName or channel type. Service names have no service FK and no rename cascade. Read all channels separately when performing an explicitly authorized cleanup; deleting a service does not delete its channels.
|
||||
- POST and PUT require a nonempty `serviceId` belonging to an existing service; a missing, empty or unknown ID returns 400/InvalidRequest. A raw `serviceName` cannot create or reassign a channel. PUT changes service assignment, settings, priority, limits, warning threshold and active state, retaining channel ID/type and usage. Create/Edit offer the same existing-services dropdown, including editable assignment on Edit; validation errors reload it. A blank password on reassignment preserves the saved value if the SMTP connection is unchanged.
|
||||
- RabbitMQ `SendEmailMessageData.ServiceName` is unchanged. The worker resolves the exact current name to a service UUID and chooses active channels by that ID and priority. The filtered GET resolves the same way (an unknown name returns an empty list). The migration-only legacy alias is not a runtime alias.
|
||||
- See [email channel service migration](email-channel-service-link.md) for backfill, failure handling and rollout compatibility.
|
||||
- SMTP HTTP lifecycle tests cover create/read/update/filter/all/delete. The repository ID lookup is a no-tracking domain projection so an update can attach its mapped entity without an EF tracking conflict. No SMTP send or owner-channel cleanup is implied by these tests.
|
||||
|
||||
## Responses
|
||||
@@ -111,3 +113,6 @@ The revision is an optimistic-concurrency token, not a historical snapshot or re
|
||||
Disposable PostgreSQL integration tests cover service lookup/rename, whole-definition reads, create/update, required arrays/boolean presence, exact codes, explicit removals, empty definitions, retention of omitted versions and their IDs/content/timestamps, shared contract replacement, invalid-save atomicity, duplicate/stale conflicts, concurrent requests, encoded keys and creation-Location round trips, and legacy-write revision invalidation. The full existing suite remains the regression gate for administration, migrations and the worker. Separate local HTTP checks exercise the host Web against task-owned infrastructure; no owner database or running Rider environment is used. Deployment/tunnel/Kubernetes validation is not implied by these tests.
|
||||
|
||||
On 2026-10-17 the solution build and all 148 tests passed after the REST CRUD, 201/Location and administration follow-ups (32 service/worker tests and 116 PostgreSQL integration tests, including 29 new definition-API cases and 3 administration-navigation checks). A separate host Web at `127.0.0.1:52656`, backed by the task-owned PostgreSQL container on port `58519`, passed service reads, exact encoded-key round trips (slash/literal `%2F`/Unicode), 201-created Location round trips and 200-updates, omitted-version retention, duplicate/stale conflicts, required-boolean rejection with no changes, final-version removal retaining the definition, required-key errors, and exact-name case checks. Its OpenAPI document exposed the typed response schemas and required request fields. The task Web and disposable database were stopped/removed afterward. No owner database, containers or Rider process were modified. Existing Microsoft.OpenApi NU1903 dependency warnings remain.
|
||||
|
||||
Push channels also reference the service by ID and block service deletion while linked.
|
||||
Reassign the push channel first; see [Push Channels](android-push.md).
|
||||
|
||||
@@ -17,7 +17,7 @@ Codes are case-sensitive exact identifiers. `ru`, `ru-RU`, `en` and `EN` remain
|
||||
|
||||
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.
|
||||
`/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 must use the current names. Email channels keep their stable service-ID association and display the renamed service automatically.
|
||||
|
||||
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.
|
||||
|
||||
@@ -37,7 +37,7 @@ Service and template editor breadcrumbs appear above the editor card, followed b
|
||||
|
||||
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.
|
||||
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 (including an empty definition) or email channel references the service. Reassign or explicitly remove channels first; deletion never cascades to them. 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.
|
||||
|
||||
@@ -108,3 +108,6 @@ The service-scoped navigation and all-language tab revision was validated separa
|
||||
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.
|
||||
|
||||
Push channels also reference the service by ID and block service deletion while linked.
|
||||
Reassign the push channel first; see [Push Channels](android-push.md).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Supported incoming message types
|
||||
|
||||
The Worker uses `SupportedMessageType` for its two incoming commands:
|
||||
|
||||
| AMQP MessageType (unchanged) | Public enum | Consumer queue |
|
||||
| --- | --- | --- |
|
||||
| `Notification.SendEmail.v1` | `SendEmailV1` | `notification.send-email` |
|
||||
| `Notification.SendPush.v1` | `SendPushV1` | `notification.send-push` |
|
||||
|
||||
`SupportedMessageTypeParser` maps the transport string at the validation boundary.
|
||||
Matching is exact and case-sensitive. Unknown versions, missing values, whitespace
|
||||
variants, enum names and numeric strings return no enum value and are rejected.
|
||||
Each consumer's delivery validator accepts only its own enum member; a supported
|
||||
command sent to the other consumer is also rejected before delivery.
|
||||
|
||||
The public enum lives in `HrynCo.NotificationService.Contracts/Messages/SupportedMessageType.cs`,
|
||||
in namespace `HrynCo.NotificationService.Contracts.Messages`, and is included in the existing
|
||||
`HrynCo.NotificationService.Contracts` package. The Worker parser remains internal and all
|
||||
consumers, validators and tests reference this single Contracts enum. It does not change RabbitMQ headers, serialized
|
||||
payloads, queues, producers or the shared RabbitMQ library. Result messages are not
|
||||
incoming delivery commands and are outside this enum's scope. Existing payload and
|
||||
correlation validation remains unchanged.
|
||||
|
||||
The Push Channels change removes the draft Firebase project/access-token fields from
|
||||
SendPushMessageData. The enum and AMQP header stay unchanged; see
|
||||
[push channels](android-push.md) for producer compatibility and rollout order.
|
||||
Reference in New Issue
Block a user