Scan start overwrote profiles.POST /api/scan/start called _save_settings() with the sidebar state, replacing the active profile's user_ids (dropping Google-only users and anything hidden by the role filter). With no profile active it overwrote profiles[0]. Scan start no longer writes profiles; profile_id is audit-only.
Profile editor dropped accounts on save. The account list was built from S._allUsers only, so manually added accounts, users not yet loaded, and users gone from the directory weren't rendered and were lost on the next save. They now render as checked "Manual" / "Not loaded" rows. user_ids: "all" is preserved when saved before users load.
Google-only users weren't ticked when applying a profile. Pending profile user IDs were cleared before the async Google merge added google:<email> users. They're now kept until matched or the user list is final, and _mergeGoogleUsers() re-applies them.
Sidebar "add user" accounts were dropped from every scan. They had no platform, and getSelectedUsers() required platform === 'm365'. A missing platform is now treated as M365.
Profile-editor manual accounts were stored as manual:<email>, which isn't a Graph user id, and applying the profile never put them in the sidebar. The editor now looks the email up via /api/users/lookup and stores the directory id with name/email (falling back to manual:<email> only if the lookup fails). Applying a profile adds its manual accounts to the sidebar and resolves legacy manual:<email> entries, logging any that can't be found.
Scan-time fallback.scan_engine._resolve_manual_users() resolves remaining manual:<email> ids (e.g. from scheduled scans) via the new M365Connector.lookup_user(). Unresolvable accounts are reported as scan errors and skipped — never widened to "all users".
Testing
3 regression tests in TestScanStartLeavesProfilesAlone (fail on old code, pass now).
7 tests in tests/test_manual_users.py for scan-time resolution (resolve, skip + report, dedupe, no widening) and the lookup route.
Full suite: 237 passed.
JS changes verified in an isolated harness loading the real profiles.js/users.js modules with stubbed APIs (no JS test runner in the repo). Not tested against a live M365 tenant.
Known limitation
Manual accounts are M365 only — an email that exists only in Google Workspace can't be added manually.
## Problem
Selecting a profile and adding users to it was unreliable — users silently disappeared from profiles, and manually added accounts were never scanned.
## Profiles losing users (bbe9c62)
1. **Scan start overwrote profiles.** `POST /api/scan/start` called `_save_settings()` with the sidebar state, replacing the active profile's `user_ids` (dropping Google-only users and anything hidden by the role filter). With no profile active it overwrote `profiles[0]`. Scan start no longer writes profiles; `profile_id` is audit-only.
2. **Profile editor dropped accounts on save.** The account list was built from `S._allUsers` only, so manually added accounts, users not yet loaded, and users gone from the directory weren't rendered and were lost on the next save. They now render as checked "Manual" / "Not loaded" rows. `user_ids: "all"` is preserved when saved before users load.
3. **Google-only users weren't ticked when applying a profile.** Pending profile user IDs were cleared before the async Google merge added `google:<email>` users. They're now kept until matched or the user list is final, and `_mergeGoogleUsers()` re-applies them.
## Manual accounts never scanned (1c79ac1)
1. **Sidebar "add user" accounts were dropped from every scan.** They had no `platform`, and `getSelectedUsers()` required `platform === 'm365'`. A missing platform is now treated as M365.
2. **Profile-editor manual accounts were stored as `manual:<email>`**, which isn't a Graph user id, and applying the profile never put them in the sidebar. The editor now looks the email up via `/api/users/lookup` and stores the directory id with name/email (falling back to `manual:<email>` only if the lookup fails). Applying a profile adds its manual accounts to the sidebar and resolves legacy `manual:<email>` entries, logging any that can't be found.
3. **Scan-time fallback.** `scan_engine._resolve_manual_users()` resolves remaining `manual:<email>` ids (e.g. from scheduled scans) via the new `M365Connector.lookup_user()`. Unresolvable accounts are reported as scan errors and skipped — never widened to "all users".
## Testing
- 3 regression tests in `TestScanStartLeavesProfilesAlone` (fail on old code, pass now).
- 7 tests in `tests/test_manual_users.py` for scan-time resolution (resolve, skip + report, dedupe, no widening) and the lookup route.
- Full suite: 237 passed.
- JS changes verified in an isolated harness loading the real `profiles.js`/`users.js` modules with stubbed APIs (no JS test runner in the repo). Not tested against a live M365 tenant.
## Known limitation
Manual accounts are M365 only — an email that exists only in Google Workspace can't be added manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Starting an M365 scan wrote the sidebar state back into the active profile
via _save_settings(), replacing its user_ids with the sidebar selection
(minus Google-only users and anything hidden by the role filter). With no
profile active it overwrote profiles[0]. Profiles are loaders, so scan
start no longer writes them; profile_id is audit-only.
The profile editor built its account list from S._allUsers only, so
manually added accounts, users not yet loaded, and users gone from the
directory were not rendered and got dropped on the next save. They now
render as checked "Manual" / "Not loaded" rows, and user_ids "all" is kept
when saved before users load.
Applying a profile cleared the pending user IDs before the un-awaited
Google merge had added google:<email> users, leaving them unticked.
Pending IDs are now kept until all are matched or the user list is final,
and _mergeGoogleUsers() re-applies them. A saved google:<email> also
matches an M365 user merged to platform 'both'.
Adds regression tests for scan start leaving profiles untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Manual accounts were never scanned:
- Sidebar "add user" entries had no platform, and getSelectedUsers()
required platform === 'm365', so they were dropped from every scan.
- Profile-editor manual accounts were stored as "manual:<email>", which
is not a Graph user id, and applying the profile never put them in the
sidebar list, so they could not be selected.
Now the editor looks the email up via /api/users/lookup and stores the
directory id plus name/email (falling back to manual:<email> only when the
lookup fails). Applying a profile adds its manual accounts to the sidebar
and resolves legacy manual:<email> entries, logging any that can't be
found. The scan engine resolves remaining manual:<email> ids (e.g. from
scheduled scans) via the new M365Connector.lookup_user(), skipping and
reporting failures without widening to all users.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
Selecting a profile and adding users to it was unreliable — users silently disappeared from profiles, and manually added accounts were never scanned.
Profiles losing users (
bbe9c62)POST /api/scan/startcalled_save_settings()with the sidebar state, replacing the active profile'suser_ids(dropping Google-only users and anything hidden by the role filter). With no profile active it overwroteprofiles[0]. Scan start no longer writes profiles;profile_idis audit-only.S._allUsersonly, so manually added accounts, users not yet loaded, and users gone from the directory weren't rendered and were lost on the next save. They now render as checked "Manual" / "Not loaded" rows.user_ids: "all"is preserved when saved before users load.google:<email>users. They're now kept until matched or the user list is final, and_mergeGoogleUsers()re-applies them.Manual accounts never scanned (
1c79ac1)platform, andgetSelectedUsers()requiredplatform === 'm365'. A missing platform is now treated as M365.manual:<email>, which isn't a Graph user id, and applying the profile never put them in the sidebar. The editor now looks the email up via/api/users/lookupand stores the directory id with name/email (falling back tomanual:<email>only if the lookup fails). Applying a profile adds its manual accounts to the sidebar and resolves legacymanual:<email>entries, logging any that can't be found.scan_engine._resolve_manual_users()resolves remainingmanual:<email>ids (e.g. from scheduled scans) via the newM365Connector.lookup_user(). Unresolvable accounts are reported as scan errors and skipped — never widened to "all users".Testing
TestScanStartLeavesProfilesAlone(fail on old code, pass now).tests/test_manual_users.pyfor scan-time resolution (resolve, skip + report, dedupe, no widening) and the lookup route.profiles.js/users.jsmodules with stubbed APIs (no JS test runner in the repo). Not tested against a live M365 tenant.Known limitation
Manual accounts are M365 only — an email that exists only in Google Workspace can't be added manually.
🤖 Generated with Claude Code