From 1e5f450b9bf08ebf17d6a040f35fb4962c9c2091 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:00:06 +0800 Subject: [PATCH 01/19] bookmark cleanup --- src/hooks/use-user-bookmarks.js | 86 +------------------------------ src/layouts/side-nav-bookmarks.js | 22 ++++---- src/layouts/top-nav.js | 9 +--- 3 files changed, 16 insertions(+), 101 deletions(-) diff --git a/src/hooks/use-user-bookmarks.js b/src/hooks/use-user-bookmarks.js index 7427ea5c06f0..bbb977522942 100644 --- a/src/hooks/use-user-bookmarks.js +++ b/src/hooks/use-user-bookmarks.js @@ -1,9 +1,7 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useMemo } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { ApiGetCall, ApiPostCall } from "../api/ApiCall"; -const SETTINGS_STORAGE_KEY = "app.settings"; - const sanitizeBookmark = (bookmark) => { if (!bookmark || typeof bookmark !== "object") { return null; @@ -43,47 +41,6 @@ const normalizeBookmarks = (value) => { return []; }; -const getLocalStoredBookmarks = () => { - if (typeof window === "undefined") { - return []; - } - - try { - const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY); - if (!restored) { - return []; - } - - const parsed = JSON.parse(restored); - return normalizeBookmarks(parsed?.bookmarks); - } catch { - return []; - } -}; - -const clearLocalStoredBookmarks = () => { - if (typeof window === "undefined") { - return; - } - - try { - const restored = window.localStorage.getItem(SETTINGS_STORAGE_KEY); - if (!restored) { - return; - } - - const parsed = JSON.parse(restored); - if (!parsed || typeof parsed !== "object" || !Object.prototype.hasOwnProperty.call(parsed, "bookmarks")) { - return; - } - - delete parsed.bookmarks; - window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(parsed)); - } catch { - return; - } -}; - const getBookmarksFromSettings = (settingsData) => { if (!settingsData) { return []; @@ -102,8 +59,6 @@ const getBookmarksFromSettings = (settingsData) => { export const useUserBookmarks = () => { const queryClient = useQueryClient(); - const localMigrationComplete = useRef(false); - const localMigrationInFlight = useRef(false); const userSettings = ApiGetCall({ url: "/api/ListUserSettings", @@ -163,47 +118,10 @@ export const useUserBookmarks = () => { [persistBookmarks] ); - useEffect(() => { - if (localMigrationComplete.current || localMigrationInFlight.current) { - return; - } - - if (!auth.data?.clientPrincipal?.userDetails) { - return; - } - - if (bookmarks.length > 0) { - localMigrationComplete.current = true; - return; - } - - const localBookmarks = getLocalStoredBookmarks(); - if (localBookmarks.length === 0) { - localMigrationComplete.current = true; - return; - } - - localMigrationInFlight.current = true; - const didPost = persistBookmarks(localBookmarks, { - onSuccess: () => { - clearLocalStoredBookmarks(); - localMigrationInFlight.current = false; - localMigrationComplete.current = true; - }, - onError: () => { - localMigrationInFlight.current = false; - }, - }); - - if (!didPost) { - localMigrationInFlight.current = false; - } - }, [auth.data?.clientPrincipal?.userDetails, bookmarks.length, persistBookmarks]); - return { bookmarks, setBookmarks, isLoading: userSettings.isLoading, isSaving: saveBookmarksPost.isPending, }; -}; \ No newline at end of file +}; diff --git a/src/layouts/side-nav-bookmarks.js b/src/layouts/side-nav-bookmarks.js index 04f4a978609a..0ae0ec7abdec 100644 --- a/src/layouts/side-nav-bookmarks.js +++ b/src/layouts/side-nav-bookmarks.js @@ -522,16 +522,18 @@ export const SideNavBookmarks = ({ collapse = false }) => { )} - { - e.preventDefault(); - removeBookmark(bookmark.path); - }} - sx={{ p: "2px" }} - > - - + {!locked && ( + { + e.preventDefault(); + removeBookmark(bookmark.path); + }} + sx={{ p: "2px" }} + > + + + )} diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 10bdcefd9581..fb5c7483e20f 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -54,7 +54,7 @@ export const TopNav = (props) => { const mdDown = useMediaQuery((theme) => theme.breakpoints.down('md')) const showPopoverBookmarks = settings.bookmarkPopover === true const reorderMode = settings.bookmarkReorderMode || 'arrows' - const locked = settings.bookmarkLocked ?? false + const locked = settings.bookmarkLocked ?? true const handleThemeSwitch = useCallback(() => { const themeName = settings.currentTheme?.value === 'light' ? 'dark' : 'light' settings.handleUpdate({ @@ -590,18 +590,13 @@ export const TopNav = (props) => { )} - {!(reorderMode === 'drag' && locked) && ( + {!locked && ( { e.preventDefault() - if (locked) { - triggerLockFlash() - return - } removeBookmark(bookmark.path) }} - sx={{ ...(locked && { opacity: 0.4 }) }} > From 3c82750d10f50983c940e9b4069eb5b4b002320f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:21:04 +0800 Subject: [PATCH 02/19] Warning when converting mailbox that is over 49GB --- .../CippWizard/CippWizardOffboarding.jsx | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 990cb9d35b11..2fc5947c22bf 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -12,9 +12,13 @@ import CippWizardStepButtons from './CippWizardStepButtons' import CippFormComponent from '../CippComponents/CippFormComponent' import { CippFormCondition } from '../CippComponents/CippFormCondition' import { useWatch } from 'react-hook-form' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { Grid } from '@mui/system' import { useSettings } from '../../hooks/use-settings' +import { ApiGetCall } from '../../api/ApiCall' + +// Shared mailboxes are capped at 50 GiB without a license; warn at 49 GiB. +const SHARED_MAILBOX_WARN_BYTES = 49 * 1024 ** 3 export const CippWizardOffboarding = (props) => { const { postUrl, formControl, onPreviousStep, onNextStep, currentStep } = props @@ -24,6 +28,40 @@ export const CippWizardOffboarding = (props) => { const userSettingsDefaults = useSettings().userSettingsDefaults const disableForwarding = useWatch({ control: formControl.control, name: 'disableForwarding' }) const deleteUser = useWatch({ control: formControl.control, name: 'DeleteUser' }) + const convertToShared = useWatch({ control: formControl.control, name: 'ConvertToShared' }) + + // Pull cached mailbox sizes (storageUsedInBytes, keyed by UPN) only when relevant + const mailboxUsage = ApiGetCall({ + url: '/api/ListMailboxes', + data: { tenantFilter: currentTenant?.value, UseReportDB: true }, + queryKey: `OffboardingMailboxUsage-${currentTenant?.value}`, + waiting: !!convertToShared && !!currentTenant?.value && selectedUsers?.length > 0, + }) + + // Selected mailboxes whose cached size would exceed the shared-mailbox limit + const oversizedMailboxes = useMemo(() => { + if (!convertToShared || !mailboxUsage.isSuccess || !Array.isArray(mailboxUsage.data)) { + return [] + } + const selectedUpns = (selectedUsers || []).map((u) => + (u?.value ?? u)?.toString().toLowerCase(), + ) + return mailboxUsage.data + .filter((mb) => { + const upn = mb?.UPN?.toString().toLowerCase() + const bytes = Number(mb?.storageUsedInBytes) + return ( + upn && + selectedUpns.includes(upn) && + Number.isFinite(bytes) && + bytes >= SHARED_MAILBOX_WARN_BYTES + ) + }) + .map((mb) => ({ + upn: mb.UPN, + sizeGB: (Number(mb.storageUsedInBytes) / 1024 ** 3).toFixed(1), + })) + }, [convertToShared, mailboxUsage.isSuccess, mailboxUsage.data, selectedUsers]) useEffect(() => { if (selectedUsers.length >= 3) { @@ -383,6 +421,21 @@ export const CippWizardOffboarding = (props) => { formControl={formControl} /> + {convertToShared && oversizedMailboxes.length > 0 && ( + + The following mailbox{oversizedMailboxes.length > 1 ? 'es' : ''} exceed or are near + the 50 GB shared mailbox limit. Converting to shared may fail, or the mailbox may + stop receiving mail once unlicensed, unless an Exchange Online Plan 2 license is + retained: + + {oversizedMailboxes.map((mb) => ( +
  • + {mb.upn} ({mb.sizeGB} GB) +
  • + ))} +
    +
    + )} From ca78e05c8d02c71e4916af1d9a4a3f241d0d3fd0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:19:42 +0800 Subject: [PATCH 03/19] multi post action for multiple spo site cleanup --- src/pages/teams-share/sharepoint/index.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index 42f08fdc0486..b39b143c6eee 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -264,15 +264,20 @@ const Page = () => { defaultvalues: { BatchDeleteMode: '2', }, - customDataformatter: (row, action, formData) => ({ - tenantFilter: row.Tenant ?? tenantFilter, - SiteUrl: row.webUrl, - BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), - DeleteOlderThanDays: - formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, - MajorVersionLimit: - formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, - }), + customDataformatter: (row, action, formData) => { + const formatRow = (singleRow) => ({ + tenantFilter: singleRow.Tenant ?? tenantFilter, + SiteUrl: singleRow.webUrl, + BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), + DeleteOlderThanDays: + formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, + MajorVersionLimit: + formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + }) + // When multiple rows are selected, row is an array. Returning an array + // makes CippApiDialog send one request per row (bulk request mode). + return Array.isArray(row) ? row.map(formatRow) : formatRow(row) + }, multiPost: false, }, ] From ff8197060250dbffdd3a65b0392329695b00d184 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:18:06 +0200 Subject: [PATCH 04/19] required = true --- src/pages/tenant/manage/drift.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 97bcd6346c36..eabac2dfb1c6 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -2068,6 +2068,7 @@ const ManageDriftPage = () => { type: 'textField', name: 'reason', label: 'Reason for change (Mandatory)', + required: true, }, ...(actionData.data?.deviations?.some((d) => d.status === 'DeniedRemediate') ? [ From 383df0af78fb667bc28b93ef943f7c49bc6c8770 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:17:16 +0800 Subject: [PATCH 05/19] repair gdap role mapping action --- .../CippSettings/CippGDAPResults.jsx | 76 +++++++++++++++++-- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/src/components/CippSettings/CippGDAPResults.jsx b/src/components/CippSettings/CippGDAPResults.jsx index 46d505a4535d..306c7451eb32 100644 --- a/src/components/CippSettings/CippGDAPResults.jsx +++ b/src/components/CippSettings/CippGDAPResults.jsx @@ -1,15 +1,34 @@ -import { Alert, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; +import { Alert, Button, List, ListItem, Skeleton, SvgIcon, Typography } from "@mui/material"; import { Cancel, CheckCircle, Warning } from "@mui/icons-material"; import { CippPropertyList } from "../CippComponents/CippPropertyList"; -import { XMarkIcon } from "@heroicons/react/24/outline"; +import { WrenchIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { CippOffCanvas } from "../CippComponents/CippOffCanvas"; import { CippDataTable } from "../CippTable/CippDataTable"; +import { ApiPostCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; import { useEffect, useState } from "react"; export const CippGDAPResults = (props) => { const { executeCheck, offcanvasVisible, setOffcanvasVisible, importReport, setCardIcon } = props; const [results, setResults] = useState({}); + const repairRoleMappings = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ExecAccessChecks-GDAP"], + }); + + const handleRepairRoleMappings = () => { + repairRoleMappings.mutate({ + url: "/api/ExecGDAPRepairRoleMappings", + data: {}, + queryKey: "RepairGDAPRoleMappings", + }); + }; + + const hasRoleMappingIssues = results?.Results?.RoleMappingResults?.some( + (item) => item?.Status === "Stale" || item?.Status === "Missing", + ); + useEffect(() => { if (importReport) { setResults(importReport); @@ -19,7 +38,11 @@ export const CippGDAPResults = (props) => { }, [executeCheck, importReport]); useEffect(() => { - if (results?.Results?.GDAPIssues?.length > 0 || results?.Results?.MissingGroups?.length > 0) { + if ( + results?.Results?.GDAPIssues?.length > 0 || + results?.Results?.MissingGroups?.length > 0 || + hasRoleMappingIssues + ) { setCardIcon(); } else { setCardIcon(); @@ -77,6 +100,15 @@ export const CippGDAPResults = (props) => { successMessage: "No Global Admin relationships found", failureMessage: "Global Admin relationships found", }, + { + resultProperty: "RoleMappingResults", + matchProperty: "Status", + match: "^(Stale|Missing)$", + count: 0, + successMessage: "All GDAP role mappings reference existing security groups", + failureMessage: + "One or more GDAP role mappings reference stale or missing security groups. Click Details to repair.", + }, ]; const propertyItems = [ @@ -154,13 +186,16 @@ export const CippGDAPResults = (props) => { }} extendedInfo={[]} > - {results?.Results?.GDAPIssues?.length > 0 && ( + {results?.Results?.GDAPIssues?.filter((issue) => issue.Category !== "RoleMapping") + .length > 0 && ( <> issue.Category !== "RoleMapping", + )} simpleColumns={["Tenant", "Type", "Issue", "Link"]} /> @@ -178,6 +213,37 @@ export const CippGDAPResults = (props) => { )} + {results?.Results?.RoleMappingResults?.length > 0 && ( + <> + + + + + } + > + Repair Role Mappings + + ) + } + data={results?.Results?.RoleMappingResults} + simpleColumns={["RoleName", "GroupName", "GroupId", "Status", "Message"]} + /> + + )} + {results?.Results?.Memberships?.filter( (membership) => membership?.["@odata.type"] === "#microsoft.graph.group", ).length > 0 && ( From 0e110a2e416c96f1353c978774c4f3fa1f12e407 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:19:13 +0800 Subject: [PATCH 06/19] Colliding query keys --- src/components/CippComponents/CippAutocomplete.jsx | 5 ++++- src/components/CippFormPages/CippAddEditUser.jsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 075f47ed29a1..04dd59da8e44 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -146,7 +146,10 @@ export const CippAutoComplete = React.forwardRef((props, ref) => { const currentTenant = api?.tenantFilter ? api.tenantFilter : useSettings().currentTenant useEffect(() => { if (actionGetRequest.isSuccess && !actionGetRequest.isFetching) { - const lastPage = actionGetRequest.data?.pages[actionGetRequest.data.pages.length - 1] + // Guard against a non-paginated cache shape (e.g. when a queryKey is accidentally shared + // with a useQuery/ApiGetCall consumer that stores a plain array instead of { pages }). + const pages = actionGetRequest.data?.pages + const lastPage = Array.isArray(pages) ? pages[pages.length - 1] : undefined const nextLinkExists = lastPage?.Metadata?.nextLink if (nextLinkExists) { actionGetRequest.fetchNextPage() diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 9a3559190372..15a5a52782c1 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -50,7 +50,7 @@ const CippAddEditUser = (props) => { // Get all groups for the tenant const tenantGroups = ApiGetCall({ url: `/api/ListGroups?tenantFilter=${tenantDomain}`, - queryKey: `ListGroups-${tenantDomain}`, + queryKey: `TenantGroupsList-${tenantDomain}`, refetchOnMount: false, refetchOnReconnect: false, }) From 61ddc970c2db1ea63ec52e3ec0ec4f6b8f385b98 Mon Sep 17 00:00:00 2001 From: Johan Aantjes <47614276+TargetCrafter@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:38:24 +0000 Subject: [PATCH 07/19] Change "All Deviations" labels to "Selected Deviations" to prevent confusion. --- src/pages/tenant/manage/drift.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index df2a3869dc38..dd0bf49a6959 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -1948,11 +1948,11 @@ const ManageDriftPage = () => { onClick={() => handleBulkAction('accept-all-customer-specific')} > - Accept All Deviations - Customer Specific + Accept Selected Deviations - Customer Specific handleBulkAction('accept-all')}> - Accept All Deviations + Accept Selected Deviations {/* Only show delete option if there are template deviations that support deletion */} {processedDriftData.currentDeviations.some( @@ -1965,12 +1965,12 @@ const ManageDriftPage = () => { ) && ( handleBulkAction('deny-all-delete')}> - Deny All Deviations - Delete + Deny Selected Deviations - Delete )} handleBulkAction('deny-all-remediate')}> - Deny All Deviations - Remediate to align with template + Deny Selected Deviations - Remediate to align with template From 495f388088cddf2e3cfe0714d99b09a50903f287 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 17 Jun 2026 23:05:01 -0400 Subject: [PATCH 08/19] fix: tenant metric grid style --- .../CippComponents/TenantMetricsGrid.jsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/CippComponents/TenantMetricsGrid.jsx b/src/components/CippComponents/TenantMetricsGrid.jsx index 323bd44a7f9f..35eda0143286 100644 --- a/src/components/CippComponents/TenantMetricsGrid.jsx +++ b/src/components/CippComponents/TenantMetricsGrid.jsx @@ -84,12 +84,13 @@ export const TenantMetricsGrid = ({ data, isLoading }) => { sx={{ display: "flex", alignItems: "center", - gap: 1.5, - p: 2, + gap: { xs: 1, sm: 1.5 }, + p: { xs: 1, sm: 1.5, md: 2 }, border: 1, borderColor: "divider", borderRadius: 1, cursor: "pointer", + minWidth: 0, transition: "all 0.2s ease-in-out", "&:hover": { borderColor: `${metric.color}.main`, @@ -103,18 +104,24 @@ export const TenantMetricsGrid = ({ data, isLoading }) => { sx={{ bgcolor: `${metric.color}.main`, color: `${metric.color}.contrastText`, - width: 34, - height: 34, + width: { xs: 28, sm: 32, md: 34 }, + height: { xs: 28, sm: 32, md: 34 }, + flexShrink: 0, }} > - + - - + + {metric.label} - - {isLoading ? : formatNumber(metric.value)} + + {isLoading ? : formatNumber(metric.value)} From 5e8a4d941dfcfa6dbbfe1b688966587612ad6a19 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:34:16 +0800 Subject: [PATCH 09/19] Update PrivateRoute.js --- src/components/PrivateRoute.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 15b438b2c608..c5adf1d455ee 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -37,9 +37,13 @@ export const PrivateRoute = ({ children, routeType }) => { waiting: session.isSuccess && session.data?.clientPrincipal !== null, }); - // If latched as unauthenticated, always show unauthenticated page + // If latched as unauthenticated, show the access-denied page — but while a + // fresh /.auth/me probe is in flight (e.g. the post-login refetch when the tab + // regains focus), show the "logging you in" loading page instead of flashing + // access-denied. The latch still holds across idle refetches; only an active + // fetch defers to loading. if (unauthLatched) { - return ; + return session.isFetching ? : ; } // Check if the session is still loading before determining authentication status From f6a0132b9db951218a75eeb86ac61f59f1edaeb2 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:02:18 +0800 Subject: [PATCH 10/19] Update standards.json --- src/data/standards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/standards.json b/src/data/standards.json index a698010ce141..fda66153955e 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4930,7 +4930,7 @@ "name": "standards.SPFileRequests", "cat": "SharePoint Standards", "tag": [], - "helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", + "helpText": "*Requires 'Sharing Level for OneDrive and SharePoint' to be set to Anyone* Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", "docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.", "executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.", "addedComponent": [ From 5426c48b0492f71a390a10fc90e09a90ce33afe5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:06:42 +0800 Subject: [PATCH 11/19] spo version cleanup job check --- src/data/standards.json | 10 +- src/pages/teams-share/sharepoint/index.js | 136 +++++++++++++++++++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index fda66153955e..3d27a41de832 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4930,7 +4930,7 @@ "name": "standards.SPFileRequests", "cat": "SharePoint Standards", "tag": [], - "helpText": "*Requires 'Sharing Level for OneDrive and SharePoint' to be set to Anyone* Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", + "helpText": "Enables or disables File Requests for SharePoint and OneDrive, allowing users to create secure upload-only links. Optionally sets the maximum number of days for the link to remain active before expiring.", "docsDescription": "File Requests allow users to create secure upload-only share links where uploads are hidden from other people using the link. This creates a secure and private way for people to upload files to a folder. This feature is not enabled by default on new tenants and requires PowerShell configuration. This standard enables or disables this feature and optionally configures link expiration settings for both SharePoint and OneDrive.", "executiveText": "Enables secure file upload functionality that allows external users to submit files directly to company folders without seeing other submissions or folder contents. This provides a professional and secure way to collect documents from clients, vendors, and partners while maintaining data privacy and security.", "addedComponent": [ @@ -8085,8 +8085,12 @@ { "type": "number", "name": "standards.SPOVersionControl.ExpireVersionsAfterDays", - "label": "Expire Versions After Days (0 = never, when auto trim is off)", - "default": 0 + "label": "Expire Versions After Days (0 = never, otherwise 30-36500, when auto trim is off)", + "default": 0, + "validators": { + "min": { "value": 0, "message": "Use 0 for never, or 30 or more days" }, + "max": { "value": 36500, "message": "Maximum value is 36500" } + } }, { "type": "switch", diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index b39b143c6eee..2c8ae199ee97 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../layouts/index.js' import { CippTablePage } from '../../../components/CippComponents/CippTablePage.jsx' -import { Button } from '@mui/material' +import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material' import { Add, AddToPhotos, @@ -10,6 +10,7 @@ import { NoAccounts, Delete, CleaningServices, + Assessment, } from '@mui/icons-material' import Link from 'next/link' import { Stack } from '@mui/system' @@ -18,6 +19,110 @@ import { useSettings } from '../../../hooks/use-settings' import { useCippReportDB } from '../../../components/CippComponents/CippReportDBControls' import CippFormComponent from '../../../components/CippComponents/CippFormComponent' import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition' +import { CippPropertyList } from '../../../components/CippComponents/CippPropertyList' +import { ApiGetCall } from '../../../api/ApiCall' + +// Friendly labels for the SharePoint version cleanup (trim) job progress fields. +const VERSION_CLEANUP_LABELS = { + Status: 'Status', + BatchDeleteMode: 'Cleanup Mode', + RequestTimeInUTC: 'Requested (UTC)', + LastProcessTimeInUTC: 'Last Processed (UTC)', + CompleteTimeInUTC: 'Completed (UTC)', + ListsProcessed: 'Lists Processed', + ListsUpdated: 'Lists Updated', + ListsFailed: 'Lists Failed', + FilesProcessed: 'Files Processed', + VersionsProcessed: 'Versions Processed', + VersionsDeleted: 'Versions Deleted', + VersionsFailed: 'Versions Failed', + StorageReleased: 'Storage Released (bytes)', + ErrorMessage: 'Error Message', + WorkItemId: 'Work Item ID', +} +// Order in which the fields are shown. +const VERSION_CLEANUP_FIELDS = Object.keys(VERSION_CLEANUP_LABELS) + +// Renders the body of the status modal based on the fetched job progress. +const VersionCleanupStatusBody = ({ statusApi }) => { + const progress = statusApi.data?.Results + + if (statusApi.isError) { + return Failed to load cleanup job status. + } + + // No job: either an empty/blank response, or the API's explicit "NoRequestFound" status. + if ( + !statusApi.isFetching && + (progress === undefined || + progress === null || + (typeof progress === 'string' && progress.trim() === '') || + progress?.Status === 'NoRequestFound') + ) { + return No cleanup job found for this site. + } + + // Backend couldn't parse the payload and returned the raw string. + if (!statusApi.isFetching && typeof progress === 'string') { + return {progress} + } + + const propertyItems = VERSION_CLEANUP_FIELDS.filter( + (key) => progress?.[key] !== undefined && progress?.[key] !== '', + ).map((key) => ({ + label: VERSION_CLEANUP_LABELS[key], + value: String(progress[key]), + })) + + return ( + ({ label: VERSION_CLEANUP_LABELS[key], value: '' })) + } + /> + ) +} + +// Custom-component action modal: opens directly (no confirmation step) and fetches the trim +// job status for the selected site, rendering it as a property list. +const VersionCleanupStatusModal = ({ row, tenantFilter, drawerVisible, setDrawerVisible }) => { + const siteRow = Array.isArray(row) ? row[0] : row + const siteUrl = siteRow?.webUrl + const statusApi = ApiGetCall({ + url: '/api/ListSPOVersionCleanup', + data: { + tenantFilter: siteRow?.Tenant ?? tenantFilter, + SiteUrl: siteUrl, + }, + queryKey: `SPOVersionCleanupStatus-${siteUrl}`, + waiting: !!drawerVisible && !!siteUrl, + }) + + return ( + setDrawerVisible(false)} + > + + Cleanup Job Status{siteRow?.displayName ? ` — ${siteRow.displayName}` : ''} + + + + + + + + + ) +} const Page = () => { const pageTitle = 'SharePoint Sites' @@ -242,7 +347,10 @@ const Page = () => { name="DeleteOlderThanDays" label="Delete Versions Older Than (days)" formControl={formHook} - validators={{ required: 'Please enter the number of days' }} + validators={{ + required: 'Please enter the number of days', + min: { value: 30, message: 'SharePoint requires at least 30 days' }, + }} /> { formControl={formHook} validators={{ required: 'Please enter the version limit' }} /> + ), @@ -273,6 +388,10 @@ const Page = () => { formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, MajorVersionLimit: formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + MajorWithMinorVersionsLimit: + formData.BatchDeleteMode === '1' + ? parseInt(formData.MajorWithMinorVersionsLimit, 10) + : -1, }) // When multiple rows are selected, row is an array. Returning an array // makes CippApiDialog send one request per row (bulk request mode). @@ -280,6 +399,19 @@ const Page = () => { }, multiPost: false, }, + { + label: 'Check Cleanup Job Status', + icon: , + customComponent: (row, { drawerVisible, setDrawerVisible }) => ( + + ), + multiPost: false, + }, ] const offCanvas = { From a79f4100e8566c3f5e231d24be0f24c7d258f8c7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:48:40 +0800 Subject: [PATCH 12/19] login page tweaks --- src/components/PrivateRoute.js | 38 ++++++++++++++++++++++++++-------- src/pages/unauthenticated.js | 3 ++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index c5adf1d455ee..98b6ed898ad7 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -2,18 +2,43 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; + +const MAX_AUTH_ATTEMPTS = 3; export const PrivateRoute = ({ children, routeType }) => { const [unauthLatched, setUnauthLatched] = useState(false); + const [authAttempts, setAuthAttempts] = useState(0); + const lastSettleRef = useRef(0); + const authBudgetExhausted = authAttempts >= MAX_AUTH_ATTEMPTS; const session = ApiGetCall({ url: "/.auth/me", queryKey: "authmeswa", - refetchOnWindowFocus: true, + waiting: !authBudgetExhausted, + refetchOnWindowFocus: !authBudgetExhausted, staleTime: 120000, // 2 minutes }); + useEffect(() => { + const settledAt = Math.max(session.dataUpdatedAt ?? 0, session.errorUpdatedAt ?? 0); + if (session.isFetching || settledAt === 0 || settledAt === lastSettleRef.current) { + return; + } + lastSettleRef.current = settledAt; + if (session.isSuccess && session.data?.clientPrincipal) { + setAuthAttempts(0); + } else { + setAuthAttempts((n) => Math.min(n + 1, MAX_AUTH_ATTEMPTS)); + } + }, [ + session.isFetching, + session.dataUpdatedAt, + session.errorUpdatedAt, + session.isSuccess, + session.data, + ]); + // Latch the unauthenticated state so refetches from child components // don't flip us back to loading. Clear the latch when session succeeds (after login). useEffect(() => { @@ -37,13 +62,8 @@ export const PrivateRoute = ({ children, routeType }) => { waiting: session.isSuccess && session.data?.clientPrincipal !== null, }); - // If latched as unauthenticated, show the access-denied page — but while a - // fresh /.auth/me probe is in flight (e.g. the post-login refetch when the tab - // regains focus), show the "logging you in" loading page instead of flashing - // access-denied. The latch still holds across idle refetches; only an active - // fetch defers to loading. - if (unauthLatched) { - return session.isFetching ? : ; + if (unauthLatched || authBudgetExhausted) { + return ; } // Check if the session is still loading before determining authentication status diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 49c6d26861b6..b07fe13bf787 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -15,7 +15,8 @@ const Page = () => { url: "/.auth/me", queryKey: "authmeswa", staleTime: 120000, - refetchOnWindowFocus: true, + refetchOnWindowFocus: false, + refetchOnMount: false, }); const blockedRoles = ["anonymous", "authenticated"]; From 539ef35d3259709473805334db1cd91252fcfede Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:20:32 +0800 Subject: [PATCH 13/19] login tweaks --- src/components/PrivateRoute.js | 45 +++++----------------------------- src/pages/unauthenticated.js | 3 +-- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 98b6ed898ad7..208c11afb8ad 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -2,58 +2,25 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; -import { useState, useEffect, useRef } from "react"; - -const MAX_AUTH_ATTEMPTS = 3; +import { useState, useEffect } from "react"; export const PrivateRoute = ({ children, routeType }) => { const [unauthLatched, setUnauthLatched] = useState(false); - const [authAttempts, setAuthAttempts] = useState(0); - const lastSettleRef = useRef(0); - const authBudgetExhausted = authAttempts >= MAX_AUTH_ATTEMPTS; const session = ApiGetCall({ url: "/.auth/me", queryKey: "authmeswa", - waiting: !authBudgetExhausted, - refetchOnWindowFocus: !authBudgetExhausted, + refetchOnWindowFocus: true, staleTime: 120000, // 2 minutes }); useEffect(() => { - const settledAt = Math.max(session.dataUpdatedAt ?? 0, session.errorUpdatedAt ?? 0); - if (session.isFetching || settledAt === 0 || settledAt === lastSettleRef.current) { - return; - } - lastSettleRef.current = settledAt; - if (session.isSuccess && session.data?.clientPrincipal) { - setAuthAttempts(0); - } else { - setAuthAttempts((n) => Math.min(n + 1, MAX_AUTH_ATTEMPTS)); - } - }, [ - session.isFetching, - session.dataUpdatedAt, - session.errorUpdatedAt, - session.isSuccess, - session.data, - ]); - - // Latch the unauthenticated state so refetches from child components - // don't flip us back to loading. Clear the latch when session succeeds (after login). - useEffect(() => { - if ( - !session.isLoading && - !session.isFetching && - (session.isError || - null === session?.data?.clientPrincipal || - session?.data === undefined) - ) { + if (!session.isLoading && !session.isFetching && !session?.data?.clientPrincipal) { setUnauthLatched(true); - } else if (session.isSuccess && session.data?.clientPrincipal) { + } else if (session?.data?.clientPrincipal) { setUnauthLatched(false); } - }, [session.isLoading, session.isFetching, session.isError, session.isSuccess, session.data]); + }, [session.isLoading, session.isFetching, session.data]); const apiRoles = ApiGetCall({ url: "/api/me", @@ -62,7 +29,7 @@ export const PrivateRoute = ({ children, routeType }) => { waiting: session.isSuccess && session.data?.clientPrincipal !== null, }); - if (unauthLatched || authBudgetExhausted) { + if (unauthLatched) { return ; } diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index b07fe13bf787..49c6d26861b6 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -15,8 +15,7 @@ const Page = () => { url: "/.auth/me", queryKey: "authmeswa", staleTime: 120000, - refetchOnWindowFocus: false, - refetchOnMount: false, + refetchOnWindowFocus: true, }); const blockedRoles = ["anonymous", "authenticated"]; From 927714e91120c7c96cd0217c94667ddb888e6874 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:46:44 +0800 Subject: [PATCH 14/19] Sensitivity label fixes --- .../CippComponents/CippDeployCompliancePolicyDrawer.jsx | 9 ++++++--- src/pages/security/compliance/labels-templates/index.js | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx index c03c976f9f94..9ca5af4741e1 100644 --- a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx +++ b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx @@ -67,10 +67,13 @@ const MODE_CONFIG = { "Tooltip": "Confidential data, do not share externally", "Comment": "Internal-only confidential classification", "ContentType": "File, Email", + "ApplyContentMarkingHeaderEnabled": true, + "ApplyContentMarkingHeaderText": "Confidential - Internal Use Only", + "ApplyContentMarkingHeaderFontColor": "#FF0000", "EncryptionEnabled": true, - "EncryptionProtectionType": "Template", - "ContentMarkingHeaderEnabled": true, - "ContentMarkingHeaderText": "Confidential - Internal Use Only", + "EncryptionProtectionType": "UserDefined", + "EncryptionPromptUser": true, + "EncryptionDoNotForward": true, "PolicyParams": { "Name": "Confidential Label Policy", "ExchangeLocation": "All", diff --git a/src/pages/security/compliance/labels-templates/index.js b/src/pages/security/compliance/labels-templates/index.js index 78477f7735b5..092d076f5370 100644 --- a/src/pages/security/compliance/labels-templates/index.js +++ b/src/pages/security/compliance/labels-templates/index.js @@ -72,9 +72,9 @@ const Page = () => { const offCanvas = { extendedInfoFields: [ - "name", "DisplayName", - "comments", + "Name", + "Comment", "ContentType", "EncryptionEnabled", "GUID", @@ -82,7 +82,7 @@ const Page = () => { actions: actions, }; - const simpleColumns = ["name", "DisplayName", "comments", "ContentType", "EncryptionEnabled", "GUID"]; + const simpleColumns = ["DisplayName", "Name", "Comment", "ContentType", "EncryptionEnabled", "GUID"]; return ( Date: Thu, 18 Jun 2026 21:18:22 +0800 Subject: [PATCH 15/19] Update PrivateRoute.js --- src/components/PrivateRoute.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 208c11afb8ad..821233c1eca8 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -4,6 +4,13 @@ import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; import { useState, useEffect } from "react"; +// EasyAuth exposes the signed-in identity in two shapes depending on the host: +// - Static Web Apps: { clientPrincipal: { userDetails, userRoles, ... } } +// - App Service EasyAuth: [ { user_id, user_claims: [...], access_token, ... } ] +// an authenticated session must be detected from either populated shape. +const hasAuthenticatedSession = (data) => + Boolean(data?.clientPrincipal) || (Array.isArray(data) && data.length > 0); + export const PrivateRoute = ({ children, routeType }) => { const [unauthLatched, setUnauthLatched] = useState(false); @@ -14,21 +21,29 @@ export const PrivateRoute = ({ children, routeType }) => { staleTime: 120000, // 2 minutes }); + // Latch the unauthenticated state so refetches from child components don't flip us + // back to loading. Latch on a request error or a settled session with no identity; + // clear it as soon as an authenticated session (either shape) is seen. useEffect(() => { - if (!session.isLoading && !session.isFetching && !session?.data?.clientPrincipal) { + if ( + !session.isLoading && + !session.isFetching && + (session.isError || !hasAuthenticatedSession(session.data)) + ) { setUnauthLatched(true); - } else if (session?.data?.clientPrincipal) { + } else if (hasAuthenticatedSession(session.data)) { setUnauthLatched(false); } - }, [session.isLoading, session.isFetching, session.data]); + }, [session.isLoading, session.isFetching, session.isError, session.data]); const apiRoles = ApiGetCall({ url: "/api/me", queryKey: "authmecipp", retry: 2, - waiting: session.isSuccess && session.data?.clientPrincipal !== null, + waiting: session.isSuccess && hasAuthenticatedSession(session.data), }); + // If latched as unauthenticated, always show unauthenticated page if (unauthLatched) { return ; } From cacd07688de6f43055a550a6d25c4917600d09de Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:12:11 +0800 Subject: [PATCH 16/19] Update cipp-users.js --- src/pages/cipp/advanced/super-admin/cipp-users.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/cipp/advanced/super-admin/cipp-users.js b/src/pages/cipp/advanced/super-admin/cipp-users.js index 32f0534825b2..ad3b6097c147 100644 --- a/src/pages/cipp/advanced/super-admin/cipp-users.js +++ b/src/pages/cipp/advanced/super-admin/cipp-users.js @@ -14,9 +14,15 @@ const Page = () => { Manage users who can access CIPP. Users are automatically synced from your partner tenant every 15 minutes based on Entra group memberships configured on the CIPP Roles page. You can also manually add users or assign additional roles — manual assignments - are preserved independently and will not be overwritten by the sync. Users not in this - list can still log in if "Allow All Tenant Users" is enabled, but they will - only receive default (authenticated) permissions. + are preserved independently and will not be overwritten by the sync. Users assigned the + superadmin role have full access to CIPP and all other permissions applied will be ignored. + You must have at least one superadmin user in CIPP at all times, and you cannot remove the + superadmin role from a user if they are the only superadmin. If you have only one superadmin + and need to change who it is, first assign another user the superadmin role, then you can + remove the superadmin role from the original user. To allow users from outside your partner tenant + to access CIPP, you can add them as guest users in your partner tenant and assign them the + appropriate roles in CIPP or enable the multi tenant mode in the CIPP SSO tab and add the users + to the list below without needing to add them as guest users in your tenant. From 6fa16c5dd90a5dd8b9ae024a964df9919456d15e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 18 Jun 2026 11:46:33 -0400 Subject: [PATCH 17/19] chore: bump version to 10.5.3 --- package.json | 4 ++-- public/version.json | 2 +- src/layouts/index.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 94b04e986410..2f634635ef99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.5.2", + "version": "10.5.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -116,4 +116,4 @@ "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1" } -} +} \ No newline at end of file diff --git a/public/version.json b/public/version.json index 326768d361ba..1ff943c0e53a 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.5.2" + "version": "10.5.3" } diff --git a/src/layouts/index.js b/src/layouts/index.js index f3c178556ff3..0ee521c51313 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -180,6 +180,7 @@ export const Layout = (props) => { // check sub-items if (item.items && item.items.length > 0) { const filteredSubItems = filterItemsByRole(item.items).filter(Boolean) + if (filteredSubItems.length === 0) return null return { ...item, items: filteredSubItems } } From 95c31b823b11d8c7b20a28de29c821f9a826708e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:48:01 +0800 Subject: [PATCH 18/19] Update CIPPDBCacheTypes.json --- src/data/CIPPDBCacheTypes.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/data/CIPPDBCacheTypes.json b/src/data/CIPPDBCacheTypes.json index 0d0588d2b624..f86f7d59b03d 100644 --- a/src/data/CIPPDBCacheTypes.json +++ b/src/data/CIPPDBCacheTypes.json @@ -328,5 +328,10 @@ "type": "DetectedApps", "friendlyName": "Detected Apps", "description": "All detected applications with devices where each app is installed" + }, + { + "type": "IntuneAppInstallStatus", + "friendlyName": "Intune App Install Status", + "description": "Per-application install status rollup (failed/installed/pending device counts) from the AppInstallStatusAggregate report" } ] From 2403bd4842bb14973306f76d8162f6b06ac56d38 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:49:26 +0800 Subject: [PATCH 19/19] Update alerts.json --- src/data/alerts.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index e0f7ffc9b9a8..9c907c4f5e92 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -391,7 +391,7 @@ { "name": "IntunePolicyConflicts", "label": "Alert on Intune policy or app conflicts/errors", - "recommendedRunInterval": "4h", + "recommendedRunInterval": "1d", "requiresInput": true, "multipleInput": true, "inputs": [