Skip to content

Add Impending and Maintenance Messages - #321

Open
aaron-rabinowitz wants to merge 4 commits into
integrate-feature-flags-into-cellix-packagesfrom
add-impending-and-maintenance-message
Open

Add Impending and Maintenance Messages#321
aaron-rabinowitz wants to merge 4 commits into
integrate-feature-flags-into-cellix-packagesfrom
add-impending-and-maintenance-message

Conversation

@aaron-rabinowitz

@aaron-rabinowitz aaron-rabinowitz commented Aug 26, 2026

Copy link
Copy Markdown

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:

  • Add portal-aware impending and active maintenance messaging with configurable schedules, localized timestamps, and HTML content.
  • Add maintenance countdown warnings that log authenticated users out when maintenance begins.
  • Expose the current server date through GraphQL for consistent maintenance schedule evaluation.

Bug Fixes:

  • Update fast-uri and js-yaml dependency resolutions to address reported security vulnerabilities.

Enhancements:

  • Integrate maintenance status handling across community and staff application routes and layouts.
  • Provide shared maintenance-message context, components, feature-flag integration, and Storybook/test coverage.

Build:

  • Add shared dependencies and workspace package configuration required for maintenance messaging and security updates.

Tests:

  • Add coverage for the maintenance countdown warning component.

@aaron-rabinowitz
aaron-rabinowitz requested a review from a team August 26, 2026 18:53
@aaron-rabinowitz
aaron-rabinowitz requested a review from a team as a code owner August 26, 2026 18:53
@sourcery-ai

sourcery-ai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

The 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 flags

sequenceDiagram
    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
Loading

State diagram for portal maintenance messaging

stateDiagram-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
Loading

File-Level Changes

Change Details Files
Adds reusable Blob-backed feature-flag contracts and retrieval to the Cellix storage service, including UTF-8 downloads, schema validation, fallback handling, and public exports.
  • Added downloadText() with missing-blob handling.
  • Added AJV validation for feature-flag payloads and optional getFeatureFlags() configuration.
  • Updated Cellix and OCOM storage contracts, adapters, documentation, and tests.
packages/cellix/service-blob-storage/src/feature-flags.ts
packages/cellix/service-blob-storage/src/feature-flags.schema.json
packages/cellix/service-blob-storage/src/interfaces.ts
packages/cellix/service-blob-storage/src/service-blob-storage.ts
packages/cellix/service-blob-storage/src/index.ts
packages/cellix/service-blob-storage/tests/index.test.ts
packages/ocom/service-blob-storage/src/blob-storage.contract.ts
packages/ocom/service-blob-storage/src/feature-flags.payload-type.ts
packages/ocom/service-blob-storage/src/index.ts
packages/ocom/service-blob-storage/src/index.test.ts
Configures the API to expose production Blob-backed feature flags with environment-selected blob names and local fallback values.
  • Added feature-flag configuration and fallback JSON.
  • Passed feature-flag options into production Blob storage.
  • Extended bootstrap and acceptance mocks and tests for the new capability.
apps/api/src/index.ts
apps/api/src/service-config/feature-flags/index.ts
apps/api/src/service-config/feature-flags/feature-flags.local.json
apps/api/src/index.test.ts
apps/api/local-settings.e2e.json
packages/ocom-verification/acceptance-api/src/mock-application-services.ts
packages/ocom/application-services/src/contexts/community/community/index.ts
packages/ocom/context-spec/src/index.ts
Introduces shared UI feature-flag loading with remote fetch, retry, caching, Storybook behavior, and local defaults.
  • Added a provider and hook for named feature-flag lookup.
  • Fetches remote flags with retry and half-TTL refreshes while retaining fallback values.
  • Added portal-specific default flag documents and configuration to both applications.
packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-context.tsx
packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-provider.tsx
packages/ocom/ui-shared/src/components/organisms/feature-flag/use-feature-flags.tsx
packages/ocom/ui-shared/src/components/organisms/feature-flag/is-in-storybook-env.ts
packages/ocom/ui-shared/src/components/organisms/index.tsx
packages/ocom/ui-shared/src/components/organisms/feature-flag/feature-flag-provider.test.tsx
apps/ui-community/src/config/feature-flag-config.ts
apps/ui-community/src/config/feature-flag-default-values.json
apps/ui-community/src/config/feature-flag-default-values.test.ts
apps/ui-community/src/main.tsx
apps/ui-community/src/App.stories.tsx
apps/ui-staff/src/config/feature-flag-config.ts
apps/ui-staff/src/config/feature-flag-default-values.json
apps/ui-staff/src/config/feature-flag-default-values.test.ts
apps/ui-staff/src/main.tsx
Adds maintenance and impending-maintenance UI state management and messaging across community and staff portals.
  • Calculates maintenance state from feature flags and server time.
  • Displays formatted, token-substituted messages and a pre-maintenance logout countdown.
  • Wrapped application routes in the provider and integrated messages into root and authenticated layouts.
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-message-provider.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-message-context.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-message.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/impending-message.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-kickout-message.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/use-maintenance-message.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/parse-html.ts
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-message-provider.graphql
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-kickout-message.test.tsx
packages/ocom/ui-shared/src/components/organisms/maintinence-message/maintenance-kickout-message.stories.tsx
apps/ui-community/src/App.tsx
apps/ui-community/src/main.tsx
apps/ui-staff/src/App.tsx
packages/ocom/ui-community-route-accounts/src/section-layout.tsx
packages/ocom/ui-community-route-admin/src/section-layout.tsx
packages/ocom/ui-community-route-root/src/section-layout.tsx
packages/ocom/ui-staff-route-root/src/section-layout.tsx
packages/ocom/ui-staff-shared/src/section-layout.tsx
packages/ocom/ui-community-route-root/src/pages/cms-page.stories.tsx
packages/ocom/graphql/src/schema/types/server-date.graphql
packages/ocom/graphql/src/schema/types/server-date.resolvers.ts
Updates build and dependency configuration to support the new feature-flag, UI, and tooling dependencies.
  • Included JSON sources in TypeScript projects.
  • Added UI and validation dependencies and lockfile security overrides.
  • Pinned Azure Functions Core Tools in the build pipeline.
apps/api/tsconfig.json
apps/ui-community/tsconfig.json
apps/ui-community/package.json
apps/ui-staff/tsconfig.json
packages/cellix/service-blob-storage/tsconfig.json
packages/cellix/service-blob-storage/package.json
packages/ocom/service-blob-storage/tsconfig.json
packages/ocom/ui-shared/package.json
packages/ocom/ui-shared/src/vite-env.d.ts
build-pipeline/core/monorepo-build-stage.yml
pnpm-workspace.yaml
pnpm-lock.yaml

Possibly linked issues

  • #Port existing Feature Flag implementation into Cellix: PR directly implements the issue's feature flag infrastructure and additionally consumes flags for maintenance messaging.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +36 to +38
const setIntervalImmediately = (func: () => Promise<void>, interval: number) => {
void func();
return globalThis.setInterval(() => void func(), interval);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to a previous PR

Comment on lines +88 to +90
return retry(
async () => {
const response = await fetch(`${url}?${timestamp}`, { cache: 'no-store' });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related to a previous PR

@aaron-rabinowitz
aaron-rabinowitz changed the base branch from main to integrate-feature-flags-into-cellix-packages August 26, 2026 19:41
aaron-rabinowitz and others added 3 commits August 26, 2026 16:10
…essage/maintenance-message-provider.tsx

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant