Skip to content

feat: implement multi-banner system with role/group targeting#12697

Open
Airamhh wants to merge 1 commit intodanny-avila:mainfrom
Airamhh:feat/multi-banner-system
Open

feat: implement multi-banner system with role/group targeting#12697
Airamhh wants to merge 1 commit intodanny-avila:mainfrom
Airamhh:feat/multi-banner-system

Conversation

@Airamhh
Copy link
Copy Markdown
Contributor

@Airamhh Airamhh commented Apr 16, 2026

Summary

Greetings!

This PR implements a multi-banner system with role/group targeting capabilities for LibreChat

The implementation follows LibreChat's established patterns for admin features, using capability-based access control and the handler abstraction layer

Closes #12696

Features:

  • Admin Routes: READ_BANNERS and MANAGE_BANNERS capabilities for the admin panel if implemented
  • Frontend: Carousel component with auto-rotation and manual navigation
  • Groups/Roles: Banner filtering by roles, groups, and specific user IDs
  • Scheduling: Support for startDate/endDate to control banner visibility
  • Persistence: Dismissed banners tracked via localStorage
  • Migration: Script provided for existing banner data
  • Tracking: Added some tracking on the banners

PR ToDo

  • The migration script might not be necessary, the implementation could be simplified while still preserving backward compatibility
  • If the current implementation approach is acceptable, the existing Banner component can be removed, I didn’t delete it because maybe you prefer to keep it separate
  • UI/UX styling can be adjusted based on your standards or preferences

Change Type

Please delete any irrelevant options.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Translation update

Testing

I had an script that created multiple banners for roles and groups to test them since I didn't want to touch a lot the script (since the idea would be create it into the admin panel) but the update banner script could be improved

Checklist

Please delete any irrelevant options.

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • I have made pertinent documentation changes
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes
  • Any changes dependent on mine have been merged and published in downstream modules.
  • A pull request for updating the documentation has been submitted.

Copilot AI review requested due to automatic review settings April 16, 2026 17:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a multi-banner system with role/group/user targeting, scheduling, and an admin CRUD API surface, plus a new client-side banner carousel.

Changes:

  • Extended the Banner schema/model and added banner CRUD/query methods (including multi-audience filtering).
  • Added new API routes for listing active banners and for admin banner management with capability checks.
  • Updated the client to render multiple banners via a rotating carousel and added supporting hooks/queries.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
packages/data-schemas/src/schema/banner.ts Adds optional multi-banner targeting fields and several indexes.
packages/data-schemas/src/methods/banner.ts Implements multi-banner querying + CRUD, audience validation, and admin helpers.
packages/data-schemas/src/methods/banner.spec.ts Adds unit tests for the new banner methods.
packages/data-schemas/src/admin/capabilities.ts Introduces READ_BANNERS / MANAGE_BANNERS capabilities and implication.
packages/data-provider/src/schemas.ts Updates banner schema/types for new fields and optional displayTo.
packages/data-provider/src/keys.ts Adds react-query keys for banners/admin banners.
packages/data-provider/src/data-service.ts Adds data-service functions for multi-banner + admin banner CRUD.
packages/data-provider/src/api-endpoints.ts Adds /api/banner/list and admin banner endpoint builders.
packages/api/src/admin/index.ts Exports the new admin banners handlers.
packages/api/src/admin/banners.ts Adds admin banner CRUD handlers (create/list/get/update/delete/toggle).
config/migrate-banners.js Adds a migration script to backfill defaults for new banner fields.
client/src/routes/Root.tsx Switches from legacy single Banner to the new BannerCarousel.
client/src/locales/en/translation.json Adds i18n strings for carousel navigation/dismiss actions.
client/src/hooks/Banners/useBannerRotation.ts Adds rotation/controls hook for banner carousel behavior.
client/src/hooks/Banners/index.ts Exports the new banner rotation hook.
client/src/data-provider/index.ts Exports new banners query hooks.
client/src/data-provider/Banners/useBannersQuery.ts Adds query hook to fetch active banners list.
client/src/data-provider/Banners/useBannerQuery.ts Adds legacy “first banner” query hook.
client/src/data-provider/Banners/index.ts Barrel export for banner query hooks.
client/src/components/Banners/index.ts Exports BannerCarousel alongside legacy Banner.
client/src/components/Banners/BannerCarousel.tsx New carousel UI with sanitization, rotation, and dismissal behavior.
client/src/components/Banners/Banner.tsx Adds DOMPurify sanitization to legacy banner rendering.
api/server/routes/index.js Registers the new admin banners router.
api/server/routes/banner.js Adds /api/banner/list endpoint while keeping legacy /api/banner.
api/server/routes/admin/banners.js New admin banners router wired with capability middleware.
api/server/index.js Mounts /api/admin/banners route.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +15 to +18
audienceMode?: 'global' | 'role' | 'group' | 'user';
targetRoleIds?: string[];
targetGroupIds?: string[];
targetUserIds?: string[];
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

targetRoleIds is treated as role names (e.g., user.role) in getActiveBanners() and validated via Role.find({ name: { $in: roleIds } }), so calling these fields *Ids is misleading and can cause incorrect usage (e.g., passing Role ObjectIds). Consider renaming to targetRoleNames (and corresponding audience query/validation), or change the implementation to actually store and query role ObjectIds consistently.

Copilot uses AI. Check for mistakes.
$or: [{ displayTo: { $gte: now } }, { displayTo: null }],
$and: [
{
$or: [{ displayTo: { $gte: now } }, { displayTo: null }],
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The active-window filter only matches displayTo >= now or displayTo: null. Documents where displayTo is missing/undefined (common if it’s omitted on create) won’t match and will never be returned. Include { displayTo: { $exists: false } } in the $or, or persist displayTo as null when omitted.

Suggested change
$or: [{ displayTo: { $gte: now } }, { displayTo: null }],
$or: [
{ displayTo: { $gte: now } },
{ displayTo: null },
{ displayTo: { $exists: false } },
],

Copilot uses AI. Check for mistakes.
Comment thread packages/api/src/admin/banners.ts Outdated
Comment on lines +14 to +20
getBannerById: (id: string) => Promise<unknown | null>;
updateBanner: (
id: string,
updates: unknown,
bannerId?: undefined,
tenantId?: string,
) => Promise<unknown | null>;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

AdminBannersDeps.updateBanner/deleteBanner/toggleBanner include placeholder params like bannerId?: undefined, which obscures the contract and doesn’t match the underlying banner methods (they accept session?: ClientSession + tenantId?: string). Update these signatures to reflect the real parameters so the handler contract stays type-safe and understandable.

Copilot uses AI. Check for mistakes.
Comment thread api/server/routes/admin/banners.js Outdated
const requireReadBanners = requireCapability(SystemCapabilities.READ_BANNERS);
const requireManageBanners = requireCapability(SystemCapabilities.MANAGE_BANNERS);

// TODO: Still missing the capabilities integration, need to talk this out with Danny
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

This TODO is misleading: capability middleware is already wired up below (requireReadBanners/requireManageBanners). If the intent is “admin panel UI integration is missing” (or similar), please reword to reflect the actual remaining work so it doesn’t read like backend access control is incomplete.

Suggested change
// TODO: Still missing the capabilities integration, need to talk this out with Danny
// TODO: Backend capability enforcement is already wired up below; confirm what admin UI/capability exposure work still remains with Danny.

Copilot uses AI. Check for mistakes.
Comment on lines +985 to +989
export function getAdminBanners(params: AdminBannersParams = {}): Promise<AdminBannersResult> {
const queryParams = new URLSearchParams();
if (params.page) {
queryParams.append('page', params.page.toString());
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Pagination params don’t match the server: this endpoint sends page, but the server pagination helper reads offset (and derives page from it). As-is, the server will default offset to 0 and effectively always return page 1. Consider sending offset = (page - 1) * limit (or updating the server to accept page).

Copilot uses AI. Check for mistakes.
...data,
bannerId,
message: data.message.trim(),
displayFrom: data.displayFrom || new Date(),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

createBanner() doesn’t set displayTo when it’s omitted. With the current query logic in getActiveBanners(), an omitted end date should be saved as null (or the query updated), otherwise the banner may never be considered active.

Suggested change
displayFrom: data.displayFrom || new Date(),
displayFrom: data.displayFrom || new Date(),
displayTo: data.displayTo ?? null,

Copilot uses AI. Check for mistakes.
return null;
}

banner.isActive = !banner.isActive;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

toggleBanner uses banner.isActive = !banner.isActive;. If isActive is undefined on legacy documents, this sets it to true (no-op) instead of toggling from the effective default (true) to false. Consider treating undefined as true when toggling (e.g., banner.isActive = !(banner.isActive ?? true)).

Suggested change
banner.isActive = !banner.isActive;
banner.isActive = !(banner.isActive ?? true);

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
mockUser = {
_id: new Types.ObjectId('507f1f77bcf86cd799439011'),
username: 'testuser',
role: 'USER',
};
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

beforeEach assigns to mockUser = { ... }, but the file only declares let _mockUser; (and never declares mockUser). This will throw at runtime / fail linting. Declare let mockUser; (or rename _mockUser -> mockUser) and use it consistently.

Copilot uses AI. Check for mistakes.
message: z.string(),
displayFrom: z.string(),
displayTo: z.string(),
displayTo: z.string().optional(),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

displayTo can be null in existing data (e.g., config/update-banner.js sets displayTo = null when not specified). Changing this to z.string().optional() still doesn’t model null, so downstream TS types remain inaccurate. Consider z.string().nullable().optional() (or a union) to reflect actual API responses.

Suggested change
displayTo: z.string().optional(),
displayTo: z.string().nullable().optional(),

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +66
const { limit, offset } = parsePagination(req.query);
const page = Math.floor(offset / limit) + 1;
const { audienceMode, isActive } = req.query;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

parsePagination reads limit + offset, but this handler computes page from offset and ignores any page query param. The corresponding client currently sends page, so pagination will be stuck on page 1 unless the API is updated to accept page or the client is updated to send offset.

Copilot uses AI. Check for mistakes.
@Airamhh Airamhh force-pushed the feat/multi-banner-system branch from 3474c06 to de975df Compare April 16, 2026 18:32
Implements a comprehensive multi-banner system with advanced audience targeting and management capabilities.

Backend:
- Add banner schema with 23 fields including audience targeting (role, group, user)
- Create banner methods in data-schemas (CRUD + getActiveBanners, toggleBanner, listBanners)
- Add admin API handlers in packages/api/src/admin/banners.ts
- Add admin routes in api/server/routes/admin/banners.js
- Add READ_BANNERS and MANAGE_BANNERS system capabilities
- Add migration script config/migrate-banners.js
- Follow LibreChat conventions:
  * CreateBannerRequest/UpdateBannerRequest in types/banner.ts (like groups.ts, roles.ts)
  * Import types from @librechat/data-schemas (following user.ts pattern)
  * No 'unknown' or 'any' types - all properly typed with IBanner interfaces
  * Capability-based access control matching groups/roles patterns

Frontend:
- Create BannerCarousel component with auto-rotation
- Add useBannerRotation hook for automatic banner cycling
- Add React Query integration (useBannerQuery, useBannersQuery)
- Add 29 localization keys in en-US
- Render banners in Root.tsx

Data Provider:
- Add banner API endpoints and schemas
- Add banner query keys
- Add data service functions

Testing:
- Add comprehensive unit tests (25 tests, 56% coverage)
- Tests use MongoMemoryServer following agent.spec.ts pattern
- All tests passing with 0 TypeScript errors, 0 lint errors

Features:
- Global, role-based, group-based, and user-specific targeting
- Scheduled banners (displayFrom/displayTo date range)
- Priority-based ordering
- Active/inactive toggle
- Persistent/dismissable banners
- Tenant isolation support
@Airamhh Airamhh force-pushed the feat/multi-banner-system branch from de975df to 423ff8f Compare April 16, 2026 18:44
@Airamhh Airamhh marked this pull request as ready for review April 16, 2026 18:50
@Airamhh
Copy link
Copy Markdown
Contributor Author

Airamhh commented Apr 16, 2026

Yaaasss, finally passing the tests 😄

Since I know Copilot can keep me on and endless loop... I think the best thing would be if you can review it whenever you can and I can work on any potential change needed

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.

[Enhancement]: Add multi banner support

2 participants