|
| 1 | +# Product Design Document: 4.4.1 Schedule Maintenance Notification (API & Backend) |
| 2 | + |
| 3 | +**References:** |
| 4 | +* PRD: [001-infra-rollout.md#4.4. Schedule Maintenance Notification (Optional)](../../prd/001-infra-rollout.md#44-schedule-maintenance-notification-optional) |
| 5 | +* Linear Issue: [CLC-1274](https://linear.app/gitpod/issue/CLC-1274/admin-schedule-maintenance-notification) |
| 6 | +* Related PDD (Maintenance Mode): [pdd/001-infra-rollout-4.3.md](./001-infra-rollout-4.3.md) |
| 7 | + |
| 8 | +## 1. Overview |
| 9 | +This document details the technical design for the API and backend components of the "Schedule Maintenance Notification" feature. This feature allows organization owners to enable, disable, and set a custom notification message that can be displayed on the Gitpod dashboard. The actual display logic, including the use of a default message if no custom one is set, will be handled by the frontend (covered in PDD 4.4.2). |
| 10 | + |
| 11 | +## 2. Goals |
| 12 | +* Define the API endpoints for managing scheduled maintenance notification settings (enabled status and custom message). |
| 13 | +* Specify the backend logic for storing and retrieving these settings. |
| 14 | +* Detail necessary database schema modifications for the `DBTeam` entity. |
| 15 | +* Ensure the backend implementation adheres to PRD requirements R4.1, R4.2, R4.3 (backend portion), and supports R4.5. |
| 16 | + |
| 17 | +## 3. Proposed Solution (API & Backend) |
| 18 | + |
| 19 | +### 3.1. Database Schema Update (`gitpod-db`) |
| 20 | +* **Target Entity:** `DBTeam` (represents an "Organization", likely in `components/gitpod-db/src/typeorm/entity/db-team.ts`). |
| 21 | +* **Action:** Add a new column `maintenanceNotification` to the `DBTeam` entity. |
| 22 | + * **Name:** `maintenanceNotification` |
| 23 | + * **Type:** `json` (or `jsonb` if preferred by the database) |
| 24 | + * **Structure:** The JSON object will store: |
| 25 | + ```json |
| 26 | + { |
| 27 | + "enabled": boolean, |
| 28 | + "message": string | undefined |
| 29 | + } |
| 30 | + ``` |
| 31 | + * **Default Value (for the column in DB):** `{"enabled": false, "message": null}` |
| 32 | + * **Nullable (column itself):** `false` (the column should always exist, its content defines state). |
| 33 | +* **Migration:** A TypeORM migration script will be generated and applied to add this column with its default value. |
| 34 | + |
| 35 | +### 3.2. API Definition (Protobuf - Public API) |
| 36 | +* **Target File:** `components/public-api/gitpod/v1/organization.proto` (or equivalent proto definition file for OrganizationService). |
| 37 | +* **Action:** Add new RPC methods and messages to the `OrganizationService`: |
| 38 | + ```protobuf |
| 39 | + service OrganizationService { |
| 40 | + // ... existing RPCs ... |
| 41 | + |
| 42 | + // GetScheduledMaintenanceNotification retrieves the scheduled maintenance notification settings for an organization. |
| 43 | + rpc GetScheduledMaintenanceNotification(GetScheduledMaintenanceNotificationRequest) returns (GetScheduledMaintenanceNotificationResponse) {} |
| 44 | + |
| 45 | + // SetScheduledMaintenanceNotification sets the scheduled maintenance notification for an organization. |
| 46 | + rpc SetScheduledMaintenanceNotification(SetScheduledMaintenanceNotificationRequest) returns (SetScheduledMaintenanceNotificationResponse) {} |
| 47 | + } |
| 48 | + |
| 49 | + message GetScheduledMaintenanceNotificationRequest { |
| 50 | + string organization_id = 1; |
| 51 | + } |
| 52 | + |
| 53 | + message GetScheduledMaintenanceNotificationResponse { |
| 54 | + bool is_enabled = 1; |
| 55 | + string message = 2; // The custom message stored, if any. Empty or not present if no custom message is set. |
| 56 | + // The frontend will use its own default if this is empty/null and is_enabled is true. |
| 57 | + } |
| 58 | + |
| 59 | + message SetScheduledMaintenanceNotificationRequest { |
| 60 | + string organization_id = 1; |
| 61 | + bool is_enabled = 2; |
| 62 | + optional string custom_message = 3; // User-provided custom message. |
| 63 | + // If not provided or empty, the backend stores null/empty for the message. |
| 64 | + } |
| 65 | + |
| 66 | + message SetScheduledMaintenanceNotificationResponse { |
| 67 | + bool is_enabled = 1; // The new enabled state |
| 68 | + string message = 2; // The custom message that is now stored, if any. |
| 69 | + } |
| 70 | + ``` |
| 71 | +* Relevant code generation scripts will be run after this change. |
| 72 | + |
| 73 | +### 3.3. API Implementation (`components/server/src/api/organization-service-api.ts`) |
| 74 | +* The `OrganizationServiceAPI` class will implement the new RPC methods. |
| 75 | +* **`getScheduledMaintenanceNotification` Implementation:** |
| 76 | + * Input: `GetScheduledMaintenanceNotificationRequest`. |
| 77 | + * Calls `this.orgService.getScheduledMaintenanceNotificationSettings(ctxUserId(), req.organizationId)`. |
| 78 | + * Maps the internal result (e.g., `{ enabled: boolean, message: string | null }`) to `GetScheduledMaintenanceNotificationResponse`. |
| 79 | + * `response.is_enabled = internalResult.enabled;` |
| 80 | + * `response.message = internalResult.message || "";` (Return empty string if message is null). |
| 81 | +* **`setScheduledMaintenanceNotification` Implementation:** |
| 82 | + * Input: `SetScheduledMaintenanceNotificationRequest`. |
| 83 | + * Calls `this.orgService.setScheduledMaintenanceNotificationSettings(ctxUserId(), req.organizationId, req.isEnabled, req.customMessage)`. |
| 84 | + * Maps the internal result to `SetScheduledMaintenanceNotificationResponse`. |
| 85 | + * `response.is_enabled = internalResult.enabled;` |
| 86 | + * `response.message = internalResult.message || "";` |
| 87 | + |
| 88 | +### 3.4. Backend Service Logic (`components/server/src/orgs/organization-service.ts`) |
| 89 | +* The `OrganizationService` class will contain the core logic. |
| 90 | +* **Type definition for the notification settings (internal use):** |
| 91 | + ```typescript |
| 92 | + interface MaintenanceNotificationSettings { |
| 93 | + enabled: boolean; |
| 94 | + message: string | undefined; |
| 95 | + } |
| 96 | + ``` |
| 97 | +* **`getScheduledMaintenanceNotificationSettings(userId: string, orgId: string): Promise<MaintenanceNotificationSettings>` method:** |
| 98 | + * Authorization: `await this.auth.checkPermissionOnOrganization(userId, "maintenance", orgId);`. |
| 99 | + * Logic: |
| 100 | + * Fetch `DBTeam` by `orgId` using `this.teamDB.findTeamById(orgId)`. |
| 101 | + * If not found, throw `ApplicationError(ErrorCodes.NOT_FOUND)`. |
| 102 | + * Let `dbNotificationConfig = team.maintenanceNotification;` |
| 103 | + * If `dbNotificationConfig` is null or parsing fails (though TypeORM usually handles JSON column parsing): |
| 104 | + * Return `{ enabled: false, message: undefined }`. |
| 105 | + * Else (valid JSON from DB): |
| 106 | + * Return `{ enabled: dbNotificationConfig.enabled, message: dbNotificationConfig.message === null ? undefined : dbNotificationConfig.message }`. |
| 107 | +* **`setScheduledMaintenanceNotificationSettings(userId: string, orgId: string, isEnabled: boolean, customMessage?: string | null): Promise<MaintenanceNotificationSettings>` method:** |
| 108 | + * Authorization: `await this.auth.checkPermissionOnOrganization(userId, "maintenance", orgId);`. |
| 109 | + * Logic: |
| 110 | + * Fetch `DBTeam`. If not found, throw `ApplicationError(ErrorCodes.NOT_FOUND)`. |
| 111 | + * Construct the new `MaintenanceNotificationSettings` object for internal logic: |
| 112 | + ```typescript |
| 113 | + const newInternalNotificationConfig: MaintenanceNotificationSettings = { |
| 114 | + enabled: isEnabled, |
| 115 | + message: (customMessage && customMessage.trim() !== "") ? customMessage.trim() : undefined, |
| 116 | + }; |
| 117 | + ``` |
| 118 | + * Prepare the object to be stored in the DB (JSON will store `null` for `undefined` message): |
| 119 | + ```typescript |
| 120 | + const notificationConfigForDb = { |
| 121 | + enabled: newInternalNotificationConfig.enabled, |
| 122 | + message: newInternalNotificationConfig.message === undefined ? null : newInternalNotificationConfig.message, |
| 123 | + }; |
| 124 | + ``` |
| 125 | + * Prepare update payload for `this.teamDB.updateTeam(orgId, updatePayload)`: |
| 126 | + * `updatePayload.maintenanceNotification = notificationConfigForDb;` (The DB driver will handle JSON serialization). |
| 127 | + * Persist changes: `const updatedTeam = await this.teamDB.updateTeam(orgId, updatePayload);` |
| 128 | + * Analytics: |
| 129 | + ```typescript |
| 130 | + this.analytics.track({ |
| 131 | + userId, |
| 132 | + event: isEnabled ? "scheduled_maintenance_notification_enabled" : "scheduled_maintenance_notification_disabled", |
| 133 | + properties: { |
| 134 | + organization_id: orgId, |
| 135 | + has_custom_message: isEnabled && !!newInternalNotificationConfig.message, |
| 136 | + }, |
| 137 | + }); |
| 138 | + ``` |
| 139 | + * Return the `newInternalNotificationConfig` object reflecting the logical state post-update. |
| 140 | + |
| 141 | +## 4. Permissions & Auditing |
| 142 | + |
| 143 | +### 4.1. Permissions |
| 144 | +* Backend service methods (`OrganizationService`) will use `await this.auth.checkPermissionOnOrganization(userId, "maintenance", orgId)` for both `getScheduledMaintenanceNotificationSettings` and `setScheduledMaintenanceNotificationSettings`. This aligns with the permission used for managing the Maintenance Mode feature (4.3) and ensures that users who can manage one can manage the other. |
| 145 | + |
| 146 | +### 4.2. Auditing |
| 147 | +* Analytics tracking as detailed in section 3.4 covers basic auditing of enable/disable actions and presence of a custom message. |
| 148 | +* If more detailed `DbAuditLog` entries are required (e.g., logging the actual message content changes), they can be added within the `setScheduledMaintenanceNotificationSettings` method in `OrganizationService`. |
| 149 | + |
| 150 | +## 5. Open Questions / Considerations (Backend Specific) |
| 151 | +* Ensuring the default value for the `maintenanceNotification` JSON column (`{"enabled": false, "message": null}`) is correctly applied by the migration and handled if the column is ever unexpectedly null. |
| 152 | +* Error handling during JSON parsing from the DB (though TypeORM typically handles this well for JSON columns). |
| 153 | +* The `message` field in `GetScheduledMaintenanceNotificationResponse` and `SetScheduledMaintenanceNotificationResponse` will be an empty string if no custom message is set (i.e., `null` in the DB). The frontend will be responsible for interpreting this as "use frontend default". |
0 commit comments