Add Impending and Maintenance Messages - #321
Conversation
Reviewer's GuideThe PR implements end-to-end impending and maintenance messaging: feature flags are validated and served from Blob Storage with local fallbacks, UI providers retrieve and cache them, and both portals use server time and portal-specific flags to display warnings, maintenance pages, and an authenticated-user logout countdown. Sequence diagram for loading and evaluating maintenance flagssequenceDiagram
participant Browser
participant FeatureFlagProvider
participant BlobStorage
participant MaintenanceMessageProvider
participant GraphQL
Browser->>FeatureFlagProvider: FeatureFlagProvider
FeatureFlagProvider->>BlobStorage: getFeatureFlags
alt Blob document exists
BlobStorage-->>FeatureFlagProvider: validated FeatureFlagsPayload
else Blob document absent or unavailable
BlobStorage-->>FeatureFlagProvider: fallback FeatureFlagsPayload
end
FeatureFlagProvider-->>Browser: cached feature flags
MaintenanceMessageProvider->>GraphQL: serverDate
GraphQL-->>MaintenanceMessageProvider: current server time
MaintenanceMessageProvider->>FeatureFlagProvider: GetFeatureFlagByName
FeatureFlagProvider-->>MaintenanceMessageProvider: portal maintenance flags
MaintenanceMessageProvider-->>Browser: impending warning or maintenance view
State diagram for portal maintenance messagingstateDiagram-v2
[*] --> Loading
Loading --> Normal: flags resolved and upcoming is false
Loading --> Impending: server time after impending timestamp
Impending --> Approaching: start is within timeout
Approaching --> LoggedOut: countdown reaches zero
LoggedOut --> Normal: HandleLogout and navigate
Impending --> Maintenance: server time reaches start timestamp
Maintenance --> Normal: server time passes end timestamp
Normal --> Impending: upcoming maintenance window begins
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues
Fixed security issues:
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-provider.tsx" line_range="36-38" />
<code_context>
+ }, []);
+
+ useEffect(() => {
+ const setIntervalImmediately = (func: () => Promise<void>, interval: number) => {
+ void func();
+ return globalThis.setInterval(() => void func(), interval);
+ };
+
</code_context>
<issue_to_address>
**issue (bug_risk):** The initial asynchronous refresh is not cancelled on unmount. If the component unmounts while the first fetch or retry is pending, cleanup sees no interval yet, then the pending promise assigns a new interval afterward and that interval continues running against an unmounted component.
**Triggers:** When either provider unmounts before its initial asynchronous refresh completes.
**Suggested fix:** Track a disposed flag or store and clear the interval returned by the pending initialization before assigning it.
</issue_to_address>
### Comment 2
<location path="packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-provider.tsx" line_range="60-62" />
<code_context>
+ return;
+ }
+
+ try {
+ const featureFlags = await cache.fetch('featureFlagsKey');
+ if (featureFlags instanceof Object) {
+ setFeatureFlags(featureFlags);
+ }
+ } catch {
</code_context>
<issue_to_address>
**issue (bug_risk):** The provider accepts any parsed JSON object as `FeatureFlags` without validating that it has a `FeatureFlags` array. A successful response such as `{}` is stored, and `GetFeatureFlagByName` then evaluates `featureFlagList?.FeatureFlags.find(...)`, which throws because `FeatureFlags` is undefined.
**Triggers:** When the feature-flag URL returns HTTP 2xx with malformed JSON structure.
**Suggested fix:** Validate the parsed payload and only store it when `FeatureFlags` is an array of objects with the expected fields; otherwise use the fallback.
```suggestion
if (
featureFlags instanceof Object &&
Array.isArray(featureFlags.FeatureFlags) &&
featureFlags.FeatureFlags.every(
(featureFlag) =>
featureFlag instanceof Object &&
typeof featureFlag.Name === 'string' &&
typeof featureFlag.Value === 'string',
)
) {
setFeatureFlags(featureFlags);
} else {
setFeatureFlags(config.fallbackFlagValues);
}
```
</issue_to_address>
### Comment 3
<location path="packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-message-provider.tsx" line_range="118-126" />
<code_context>
+ const maintenanceStartTime = dayjs(params.maintenanceStartTimestamp);
+ const maintenanceEndTime = dayjs(params.maintenanceEndTimestamp);
+
+ if (serverTime > impendingTime && serverTime < maintenanceStartTime) {
+ setIsMaintenance(false);
+ setIsImpending(true);
+ // within 1 minute before maintenance start
+ if (maintenanceStartTime.diff(serverTime, 'seconds') <= timeoutBeforeMaintenance) {
+ setIsApproachingMaintenance(true);
+ setMaintenanceCountdown(maintenanceStartTime.diff(serverTime, 'seconds'));
+ }
+ } else if (serverTime > maintenanceStartTime && serverTime < maintenanceEndTime) {
+ setIsMaintenance(true);
+ setIsImpending(false);
</code_context>
<issue_to_address>
**issue (bug_risk):** Maintenance status uses strict comparisons for every boundary, so the UI shows neither impending nor maintenance state when the server time is exactly the impending timestamp, maintenance start timestamp, or maintenance end timestamp.
**Triggers:** When the server clock equals one of the configured maintenance timestamps.
**Suggested fix:** Use inclusive comparisons for the intended start boundaries and define whether the end timestamp is inclusive or exclusive explicitly.
```suggestion
if (serverTime >= impendingTime && serverTime < maintenanceStartTime) {
setIsMaintenance(false);
setIsImpending(true);
// within 1 minute before maintenance start
if (maintenanceStartTime.diff(serverTime, 'seconds') <= timeoutBeforeMaintenance) {
setIsApproachingMaintenance(true);
setMaintenanceCountdown(maintenanceStartTime.diff(serverTime, 'seconds'));
}
} else if (serverTime >= maintenanceStartTime && serverTime < maintenanceEndTime) {
```
</issue_to_address>
### Comment 4
<location path="packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-provider.tsx" line_range="88-90" />
<code_context>
+function fetchFeatureFlags(url: string): Promise<FeatureFlags> {
+ const timestamp = Date.now();
+
+ return retry(
+ async () => {
+ const response = await fetch(`${url}?${timestamp}`, { cache: 'no-store' });
+ if (!response.ok) {
+ throw new Error(`Feature flag request failed with status ${response.status}`);
</code_context>
<issue_to_address>
**issue (bug_risk):** The cache-busting URL is always constructed by appending `?timestamp`; when `config.url` already contains query parameters such as a SAS token, the resulting URL is malformed and the request fails, forcing fallback values.
**Triggers:** When `VITE_COMMON_FEATURE_FLAG_URL` is configured with an existing query string.
**Suggested fix:** Use `URL`/`URLSearchParams` to add or replace the cache-busting parameter while preserving existing query parameters.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const setIntervalImmediately = (func: () => Promise<void>, interval: number) => { | ||
| void func(); | ||
| return globalThis.setInterval(() => void func(), interval); |
There was a problem hiding this comment.
issue (bug_risk): The initial asynchronous refresh is not cancelled on unmount. If the component unmounts while the first fetch or retry is pending, cleanup sees no interval yet, then the pending promise assigns a new interval afterward and that interval continues running against an unmounted component.
Triggers: When either provider unmounts before its initial asynchronous refresh completes.
Suggested fix: Track a disposed flag or store and clear the interval returned by the pending initialization before assigning it.
There was a problem hiding this comment.
related to a previous PR
| return retry( | ||
| async () => { | ||
| const response = await fetch(`${url}?${timestamp}`, { cache: 'no-store' }); |
There was a problem hiding this comment.
issue (bug_risk): The cache-busting URL is always constructed by appending ?timestamp; when config.url already contains query parameters such as a SAS token, the resulting URL is malformed and the request fails, forcing fallback values.
Triggers: When VITE_COMMON_FEATURE_FLAG_URL is configured with an existing query string.
Suggested fix: Use URL/URLSearchParams to add or replace the cache-busting parameter while preserving existing query parameters.
There was a problem hiding this comment.
related to a previous PR
…essage/maintenance-message-provider.tsx Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Summary by Sourcery
Implement configurable impending and maintenance messaging across the community and staff portals, including scheduled access blocking and user sign-out.
New Features:
Bug Fixes:
Enhancements:
Build:
Tests: