IT Helper

Open IT support cases from Teams; the bot emails them to Thrive’s ServiceNow intake and relays status back — no API access required. Live org-wide since 2026-07-15. Renamed from “PSI IT Help Desk” 2026-07-16; internal names (repo paths, psi-helpdesk--* secrets, functions) keep the original helpdesk naming.


What it is

A Teams bot every PSI user has (auto-installed + pinned via app setup policy). Tell it what’s wrong in your own words → it opens a case, emails it to our MSP (Thrive, support@thrivenextgen.com) from the techsupport@progressivesurface.com shared mailbox, and mirrors every case into the internal IT Cases chat. When Thrive’s ServiceNow acknowledges (~3 min), a poller correlates the ack and hands the requester their case number + portal link in the same Teams chat.

It’s a forwarder, not a case tracker. Thrive re-assigns each case to the actual user on intake and owns the relationship (updates, closure) directly from there; the bot never tracks lifecycle, and users never see the internal PSI-nnnn refs (those live in email subjects + the mirror chat for ops correlation only).

Built because Thrive’s platform change retired their Teams ticket bot, right as our primary tech went on paternity leave. Email is an implementation detail the user never sees; the transport swaps for the ServiceNow API without UX changes if Thrive ever grants access.

Code: psi-notify-bot — shares the psi-notify-listener Function App, Bot Framework plumbing, alerts DB, and Key Vault conventions with PSI Notify Bot (separate Teams identity, same backend). Authoritative change history: BUILD_LOG.md in that repo. Plan/architecture/runbook: docs/helpdesk-bot-plan.md.


User experience

ActionResult
Type anything at the botNew-case form opens pre-filled with what you typed — only the description is required; type/urgency one tap; category/location/on-behalf-of under “More details”
💻 PSI application (ProApps) issueExpander: which app(s) + “Did you run a ProReset?” — travels to Thrive, who assign PSI’s internal team
Submit / CancelSubmit closes the card (no double-file) + “we’re opening your request” (no case number yet, deliberately); Cancel closes it with nothing filed
my cases“Your recent requests” — last 30 days, case numbers linked to the Thrive portal; a receipt lookup, not a tracker
Thrive acknowledges“🎟️ Your case CS… is open with our IT provider” + View at Thrive button
Thrive updates / resolvesForwarded as a card (redundant/duplicate notices are de-duplicated; the [External] banner is stripped from the preview); resolution notices lead ”✅ Thrive marked … resolved”
Delivery bounceHonest “IT is handling it” to the user + a red 🚨 alert in the IT Cases chat

Department-specific forms

The form tailors itself from the requester’s Entra department at open time (config-driven — no code change to adjust):

  • Process Services (dept 125): “⚠️ Is a production line down?” — Yes forces critical urgency, a [LINE DOWN] subject prefix + banner to Thrive, and a red 🚨 IT Cases mirror card.
  • HR (dept 180, + named individuals): three launch buttons — ➕ New user, 🔄 Position change, ➖ User termination — each opening a dedicated request card (see below).

Config (Function App settings): HELPDESK_LINEDOWN_DEPTS (default 125), HELPDESK_HR_DEPTS (180), HELPDESK_HR_USERS (specific UPNs that get the HR form regardless of department — for IT to see/troubleshoot: Adam Devereaux, Chris Hosakura).

HR request flows (A/R/C)

HR requests route to Thrive like any case (Thrive assigns PSI’s internal team); each carries a structured payload + an SOP checklist in the email. Full spec + decisions: docs/hr-request-worksheet.md.

  • New user (A): starts from the employee number and reads the rest from AFTEC (see below). License/groups derive from a matrix — not picked by HR. Policy: every employee gets a licensed named account.
  • Position change (C): new title/dept/manager, effective date, “set up like” model user, remove-old-access (mandatory), equipment/location for the manager.
  • Termination (R): three paths, each with its checklist — 1 full delete (shop floor) · 2 preserve & convert (mailbox→shared, manager gets OneDrive, +90-day review) · 3 keep active (atomic password+sessions+MFA reset, credentials to manager, +30-day review). “Immediately” → critical + 🚨. Sender experience silent by default (see docs/offboarding-ooto-recommendations.md).

Manager is CC’d on all HR emails.

New-user form reads AFTEC (2026-08-06)

The form used to ask HR for seven identity fields. By the time they open it, they have already entered the hire in AFTEC twice over — so it was asking them to retype work they had just finished, in both directions.

HR’s actual sequence: Paylocity ATS → candidate accepts → HR creates the employee in Paylocity (which assigns an employee ID they must verify is non-conflicting) → HR enters them in AFTEC (department, shift, hourly/salary, names, start date, rate) → a second AFTEC screen for direct/indirect, the three-letter user id (AFTEC itself rejects a conflicting one) and the in-house-subcontractor flag → only then the IT request.

So the form now asks for the employee number, reads AFTEC, shows the record back for confirmation, and asks the one thing AFTEC does not hold: the job title — which is exactly what the licensing matrix keys on (TITLE is blank on every record checked and sparsely used file-wide).

Form fieldAFTEC source
Full nameLAST.NAME + FIRST.NAME (upper-case in AFTEC; the bot title-cases it)
Initials → sAMAccountNameUSERID — uniqueness already enforced by AFTEC
DepartmentDEPT.NO
Start dateSTART.DATE (from the payroll file)
ManagerRPT.TO (an employee number) → resolved to a person in Entra, see below
Employee / TempSUBCONTRACT = Y is the in-house sub; EMPTYPE D/I is direct/indirect labour
ShiftSHIFT (D/N) — shown; door groups differ by shift
Job titlenot in AFTEC — asked

Two record-key formats, neither guessable (verified live 2026-08-06):

  • EMPLOYEE.PUBLIC.1287CO!EMPNO with the number zero-padded to four: 1!0793, 1!4200. 1!793 is a 404.
  • EMPLOYEE (payroll) — CO concatenated with that padded number, no delimiter: 10793, 14200. Follows the dictionary’s own TRANS(EMPLOYEE,CO:EMPNO,…) expression. This is where the start date lives, already converted to yyyy-MM-dd by the API.

The manager is resolved, not asked for. RPT.TO is an employee number, and Entra stamps that same number on the user — unpadded (793, not 0793, the opposite of AFTEC’s record keys). So the card filters the directory on employeeId, shows the manager by name (Dennis Pearo (DPearo@progressivesurface.com)), and offers the same “More — someone else is the manager” override the flat departments use instead of a question. Three deliberate exceptions:

  • a disabled account keeps the question and is flagged — AFTEC reporting lines point at leavers, and provisioning a hire under an unreachable manager is worse than one more field;
  • an unresolvable number says so rather than displaying a bare number;
  • AFTEC’s own derived EMAIL field (FIRST.NAME[1,1]:LAST.NAME:"@PROGRESSIVESURFACE.COM") is not used as a fallback. It matches PSI’s convention and would be right most of the time, which is not good enough for who someone reports to.

This removes most of the reason to hand-fill the supervisors lists in hr-org.json (all still empty placeholders) for AFTEC-sourced hires: a per-person answer from the ERP beats a hand-maintained list.

Identity model: the read is delegated. The listener’s system-assigned managed identity holds the UniData.ActAsUser app role on PSI.UniData.API, and each call carries X-On-Behalf-Of: <the HR person's UPN>; the API opens the UniData session as that person using their own credentials, so AFTEC’s audit shows Gina or Karen, not a service account. The header without the role is a deliberate 403, never a silent downgrade. The bot therefore holds no UniData credential of its own.

Fail-soft by design. No AFTEC record, an inactive one, a department AFTEC names that the HR list does not have, or the API being unreachable all land back on the lookup card with a plain sentence and “Enter it manually instead” one tap away — which re-opens the original department-first form. A person starting Monday must never be blocked because the ERP is slow.

Why the first deploy of this hung (fixed 2026-08-07). The lookup timed out after 101 seconds. api.progressivesurface.com is PS-PROXY at 10.150.141.9, and Azure reaches on-prem only through the Meraki vMX via the MerakiRouteTableAZ-GR route table — which was not attached to ps-flexfunc, the subnet this Flex Consumption app egresses through. DNS had been correct the whole time, which is exactly what made it look like a DNS fault. See Deploy to Azure §5.

Two things changed as a result: the subnet got the route table, and the lookup got an 8-second budget (both employee files read in parallel) with a card that distinguishes “no AFTEC employee 4200” from “couldn’t reach AFTEC just now” — a 100-second hang is worse than a fast honest answer, and Teams abandons a card action at ~15 s anyway. GET /api/alerts/status now carries a unidata block (reached, HTTP status, elapsed ms) so on-prem reachability is checkable without asking HR to tap anything.

How cases reach Thrive (two transports)

Thrive told PSI on 2026-07-28 that cases opened by email are triaged at a low priority and asked us to submit through their client portal instead. A proper ServiceNow API integration is in progress on Thrive’s side but stuck in their approval process, so the portal is the near-term path.

Email (original)Portal (new)
How Thrive sees itcontact_type = Email → low prioritycontact_type = Client Portal → normal triage
Case contacttechsupport@, re-assigned to the real user by Thrive’s SOPthe actual requester, from creation
Case numberminutes later, scraped from the ack emailreturned synchronously

portal.thrivenextgen.com is Thrive’s own Next.js app (not ServiceNow’s UI), SSO-federated to PSI’s Entra tenant, with a backend that thinly proxies ServiceNow’s Table API — POST /api/table?endpoint=cases to create, GET /api/table?table=…&query=… to read, GET /api/scriptedrest?endpoint=downstream-subcats for the cascading category picklists. It has no service-to-service auth, so a scheduled Playwright job on PS-PROXY signs techsupport@ in over Entra SSO every 6 hours and stores the session cookie as psi-helpdesk--thrive-portal-cookie; PS-PROXY specifically, because the service account’s Conditional Access exemption is scoped to that host’s egress IP.

HELPDESK_TRANSPORT (portal | email, default email) picks the primary. Any portal failure falls back to email and records a case event — a portal outage can never cost a user their request. Rollback is one app setting.

Status: code merged 2026-07-28 behind the flag, not yet enabled. Pending: the create-case request envelope (captured from the next real submission via scripts/thrive-session/capture-payload.js --submit), the techsupport@ portal credentials in Key Vault, and the CA exemption. Full detail: docs/thrive-portal-transport.md in the repo.

Architecture

User (Teams) → /api/helpdesk/messages (psi-notify-listener, Flex Consumption)
  → INSERT dbo.Cases → Graph sendMail (techsupport@ → support@thrivenextgen.com)
  → confirmation card → mirror card to IT Cases chat (activity id stored)
case-mail-poller (2-min timer) → reads techsupport@ inbox (read-only; watermark
  + claim→processed, at-least-once) → correlates [PSI-nnnn] / CS number
  → ack: case number + portal link to requester, updates mirror card in place
  → later mail: dedupe + strip [External] banner → forward; bounces → 🚨 IT Cases
  • Caller identity: all mail reaches Thrive from techsupport@, so the email carries max identity — requester name in the subject, body block with name/email/title/dept/office/phone (Graph User.Read.All), Reply-To = requester + techsupport@.
  • Mailbox never mutated (Mail.Send/Mail.Read scoped to techsupport@ only via an Exchange application access policy); dedupe lives in dbo.MailboxWatermark + dbo.CaseInboundMail.
  • Forwarded-update hygiene: Thrive sends redundant notices (observed: two identical “Resolved” 28s apart + a nudge 2 days later); the poller suppresses repeats (dedupe on subject+body, coalesce resolution notices) and strips the [External] CAUTION banner from previews.
  • Data: dbo.Cases / dbo.CaseEvents / dbo.CaseInboundMail in the alerts DB (procserv-proddata). Schema applies via POST /api/helpdesk/migrate (function key; listener MI has ddladmin) — re-run after any deploy that adds columns.

Azure resources

ResourceNameNotes
App registrationIT HelperApp ID a2fb6b1e-3cdf-4325-8db5-4f850151f76a; Graph Mail.Send, Mail.Read, User.Read.All (application, admin-consented)
Azure Botpsi-helpdesk-botPS-WEBAPPS, F0, Teams channel
Teams appIT HelperOrg catalog ID b8369c60-9e93-4c63-9179-f2a645988cd2; auto-install + pin via app setup policy; green “IT” icon
Function Apppsi-notify-listener (shared)Routes api/helpdesk/*; poller gated by HELPDESK_POLL_ENABLED=1
App settingsHELPDESK_POLL_ENABLED, HELPDESK_LINEDOWN_DEPTS, HELPDESK_HR_DEPTS, HELPDESK_HR_USERS
Secretsps-certificates-kvpsi-helpdesk--* (tenant/client id+secret, mailbox, msp-intake-email, msp-portal-case-url, chat-it-cases, teams-app-catalog-id)
Provisioning apppsi-hr-provisioningEnterprise app, SP ded5f229-2296-4db5-9cd5-d16d8f3c201b; API-driven inbound provisioning to on-prem AD (ad.ptihome.com)
Provisioning identityid-psi-provisioningUser-assigned MI 7a1571e8-5ae4-4576-86b2-76be744ed273 on psi-notify-listener; Graph SynchronizationData-User.Upload, ProvisioningLog.Read.All, Organization.Read.All
Group-add accountuserprovisioningAD account with WriteProperty on member across OU=Groups,OU=PTI — provisioning sets no group membership
Mirror chat“IT Cases”19:cf22f43c5e2147eaa0c8b434c3b50a76@thread.v2 — Dakota Cooper, Adam Devereaux, Chris Hosakura, Power Operative

Operations

  • Intake address: the psi-helpdesk--msp-intake-email secret (process-cached — restart/redeploy the listener to apply deterministically).
  • Form flavors: the HELPDESK_*_DEPTS / HELPDESK_HR_USERS app settings (comma-separated; dept = name-prefix match, users = UPN/email). Change without a deploy.
  • Schema: POST /api/helpdesk/migrate (function key) applies sql/helpdesk-schema.sql via the listener MI — run after any deploy that adds dbo.Cases columns.
  • POC / testing without Thrive: point the intake secret at an internal mailbox; scripts/Send-FakeSnowAck.ps1 simulates ServiceNow Ack/Update/Resolved.
  • Provision from scratch: scripts/Setup-HelpdeskBot.ps1 + the runbook in docs/helpdesk-bot-plan.md.
  • Telemetry: App Insights psi-notify-listener; poller logs Case PSI-nnnn acknowledged as CS…, Forwarded provider …, Suppressed a redundant provider forward …, Provider delivery FAILED ….
  • “Failed to send” + IT Helper missing from Teams search → check for store lookalikes (incident 2026-08-04). Public Teams-store apps named “It Help Desk Bot” (one with a Spanish description) outranked/shadowed the org app in client search; users messaged the lookalike and its dead endpoint returned “failed to send”, while the real bot’s telemetry showed nothing arriving. Diagnosis signature: zero helpdesk-messages requests in App Insights while the endpoint answers a direct probe, and other users (still pinned to the real app) unaffected. Fix: Teams admin center → Manage apps → block the lookalikes’ availability. Prevention worth considering: restrict third-party store apps by default in the org app permission policy.

Automated user provisioning (live)

New-user requests are moving from “Thrive provisions by hand” to Entra API-driven inbound provisioning: the listener POSTs a SCIM record to /bulkUpload, the Microsoft provisioning agent writes the user into on-prem AD, and Entra Connect syncs it back to the cloud.

Status (2026-08-13): live end to end. HELPDESK_PROVISIONING_ENABLED=1, PROVISIONING_POST_TASKS_ENABLED=1, PROVISIONING_MATRIX_SQL_ENABLED=1, and /api/alerts/status reports origin: "Sql". The Thrive path remains the fallback for a case the matrix cannot resolve or a row nobody has signed off.

Accounts are created ENABLED. The plan’s “no dormant enabled account” rule deferred the enable to a start-date job that was never built, so every hire landed disabled and stayed that way until a human noticed — Kurt Leatherman included. The rule was removed, not the accounts (#70, 2026-08-11). Anything that still says “disabled until the start date” is out of date.

Existing accounts are named on the card (2026-09-09, psi-notify-bot #105). The upload matches on employeeID, so a “new user” whose employee number already has an account becomes an Update of that account. PSI-1038 found this out as a failed rename (SamSamuel, INSUFF_ACCESS_RIGHTS) four hours after approval. At submit the listener now looks the employee number up in the directory and, when a person comes back, the approval card carries an ⚠️ notice: the account, enabled or disabled, its login, that ✅ will update it (a rehire or a conversion), and two warnings when they apply — HR’s name differs from the account’s (the job cannot rename; fix AD first or ✋), and the form’s initials are not the account’s login. dbo.CaseEvents records existing_account_detected. The tap is not blocked; what to do about a rehire is #94.

Seat check on signed rows (2026-09-09, #104). Signed policy rows name the Entra licensing group (LIC - M365 Business Premium); the SKU map now strips that prefix, so the seat guard runs for signed rows too. Before this it silently skipped them (seat_check_unknown) and only protected the drafts.

How it works

HR completes the New user card → approval card in IT Cases → ScimPayloadBuilder emits a SCIM BulkRequest → ProvisioningDispatcher POSTs it → the provisioning job applies attribute mappings → the agent creates the AD object → ProvisioningLogPollerFunction correlates the result back to the case.

It is request-driven, not a directory sync. Only users explicitly sent are touched; a user absent from a payload is never modified or deprovisioned. There is no dry-run mode — a POST creates a real AD object.

What the listener must send

SCIM attributeCarriesBecomes in AD
externalIdEmployee IDemployeeIDthe matching attribute
userNameFull UPN, FLast@progressivesurface.comuserPrincipalName, mail
nickName3-letter initialssAMAccountName
displayNameFull namedisplayName, CN — an Update that changes this is a rename, which the agent was refused on PSI-1038; confirm the mapping is Create-only in the Entra job
titleJob titletitle and description
userTypeEmployee / Temp / ContractoremployeeType
departmentCanonical NNN - Namedepartment, and the OU
enterprise manager.valueManager’s employee IDmanager (resolved to a DN)
activetrue on createthe account is created enabled

22 attribute mappings in total. OU placement is derived from the leading 3-digit department code, so a department being renamed cannot break it, and users land in OU=<NNN - Name>,OU=Users,OU=PTI. There is no separate OU for temporary staff — temp status is recorded in employeeType.

Safety posture

GuardSetting
Target object actionsAdd,UpdateDelete removed
Accidental deletion threshold5
Skip out-of-scope deletionsEnabled
AD Recycle BinEnabled, 180-day retention
Notification recipientSet (an empty value quarantines the job on start)

userPrincipalName, mail, sAMAccountName, CN and displayName are create-only — they are written once and never overwritten by a later change request. Everything else follows HR.

Where the policy lives — PSI Portal (live 2026-08-06)

Which license and which groups a 125 / Process Technician gets started life as listener/Data/licensing-matrix.json, compiled into the Function App — so changing one group meant edit, commit, redeploy, and no row had ever been signed off. That policy now lives in PSI Portal, SQL-backed in the same alerts database: the listener reads a view (dbo.vw_prov_matrix_json) on a 5-minute TTL and keeps the embedded file only as the fallback of record. Design and build order: docs/User Provisioning - on prem helper plan.md in the repo.

Live since 2026-08-06PROVISIONING_MATRIX_SQL_ENABLED=1, the schema is applied in alerts and seeded (18 departments, 8 rules, 425 group rows, 79 title refs), and /api/alerts/status reports origin: "Sql". HELPDESK_PROVISIONING_ENABLED=1. The rules that govern it:

  • A provisioning decision never comes from the fallback. PROVISIONING_MATRIX_REQUIRE_SQL (default on) turns “serving the embedded copy” into an ordinary Thrive case — a department IT deleted in SQL still exists in the built-in file.
  • Unsigned = draft. An unconfirmed department row blocks the ✅ tap. The approval card still posts (that is where IT learns the row needs confirming) with the reason, a link to the row, and ✋ Send to Thrive live. PROVISIONING_ALLOW_UNCONFIRMED=1 pushes a single pilot hire through.
  • No seat, no hire — and it resumes itself. Before uploading, the dispatcher checks free seats for the derived SKU. A dry pool parks the case as waiting_license and files a seat-purchase case with Thrive (Thrive sells the seat; it no longer creates the account). Since 2026-08-10 a 15-minute sweep re-checks parked hires and resumes the approval automatically once a seat exists, keeping the original approver’s name in the trail — before that the status was written and never read, so a hire sat parked forever and the card offered no way to retry. An unmapped or PROPOSED licence has no seat gate at all, deliberately: a confident number about the wrong pool is worse than none.
  • The approved decision is snapshotted to dbo.Cases.ProvisioningPlanJson at the tap, because by the time Entra reports the account created the matrix is minutes older and editable — the hire gets what IT approved, not what the matrix says later.
  • GET /api/alerts/status reports which matrix is in force (version, origin, departments, loadedUtc). origin: "Sql" is the only trustworthy sign that portal policy is live — no doc can tell you.

Known gaps

  • Group membership is not provisioned yet. Closed 2026-08-10. On provisioned the listener enqueues set_manager + one add_ad_group per on-prem group into dbo.prov_post_tasks (psi-portal’s table), and the userprovisioning worker on PS-PROXY claims them. Proven on case 1029: set_manager and eight groups — including all three door groups — every one done on the first attempt, in seven seconds. PROVISIONING_POST_TASKS_ENABLED is on. Entra dynamic groups (DG - <dept>, All email users, Progressive Surface) still attach themselves once the account is enabled. A 15-minute sweep cards failed tasks into IT Cases so a half-provisioned hire cannot stay invisible.
  • manager cannot be set by the provisioning job. Entra only resolves reference attributes for objects the job itself processed, so the mapping silently skips anyone else — which is why set_manager belongs to the on-prem worker, and why the Entra mapping comes out once the worker proves itself (psi-notify-bot#42).
  • Legal vs preferred name. HR does not yet supply them separately, so displayName is pinned to create-only to avoid overwriting someone’s preferred name.
  • employeeHireDate is not an on-premises AD attribute; feeding Lifecycle Workflows needs an extensionAttribute plus an Entra Connect rule.
  • AFTEC records rehires 0.2% of the time, so employment status derived from its termination date is unreliable — relevant when the input source moves from the HR form to a system feed.
DocPurpose
docs/helpdesk-bot-plan.mdDesign, forwarder model, ops runbook, MSP asks, phases
docs/hr-request-worksheet.mdHR A/R/C field/policy decisions (answered)
docs/licensing-matrix.mdOriginal dept→license/groups matrix — superseded by the SQL policy in PSI Portal
docs/User Provisioning - on prem helper plan.mdProvisioning design + build order (GHE issue #42 is the tracking issue)
docs/offboarding-ooto-recommendations.mdDeparted-mailbox sender-notification policy (for org discussion)
BUILD_LOG.mdAuthoritative dated change history

Open items (2026-08-13)

Tracked in GHE rather than here, so this list stays short and the issues stay authoritative:

  • [#93] Terminations should run through the provisioning machinery. A hire is provisioned by us; a termination is still a checklist Thrive works by hand. disable_ad_user already exists in the PS-PROXY worker and nothing enqueues it. The dangerous half of the lifecycle is the manual one.
  • [#94] Rehire / co-op conversion is a fourth HR path that works today only by accident: the SCIM matching attribute is employeeID, so a New user request with an existing number matches and UPDATES rather than creating. If Paylocity issued a NEW number for the conversion, you get a second account.
  • [#95] Provisioning only starts one way — HR, in Teams, from an employee number. A name lookup should search Entra (which also reveals an existing account, half of #94), and PSI Portal should be able to start a request rather than only watch one.
  • Portal transport go-live. HELPDESK_TRANSPORT is unset, so every case still reaches Thrive by email and is triaged at low priority — Thrive told us so on 2026-07-28. Needs a captured create-case payload, the techsupport@ portal credentials, and the CA exemption. Highest-value item on this list: it changes how quickly every PSI request gets looked at.
  • AFTEC is coarser than our matrix in at least one place. AFTEC has 110 and no 111, so a controls engineer reads as Mechanical. The intake now makes the department correctable with the reason attached (2026-08-13), but nobody has compared AFTEC’s full DEPT.NO list against the 18 in the matrix — other departments split after the ERP was populated would have the same silent failure.
  • Most policy rows are unsigned and, until each department’s license names a real LIC - * group, a hire provisions unlicensed. Both are now fixable in PSI Portal’s access matrix.
  • Sign-off and policy editing need PROVISIONING_EDITOR_UPNS set on the psi-portal App Service. The gate is fail-closed, so an empty value means nobody can edit or sign off — and it looks exactly like broken code. Set 2026-08-13 for ADevereaux + CHosakura.
  • instance in /api/alerts/status reports “local” on Flex Consumption — cosmetic, but it makes per-instance matrix state unverifiable from outside.
  • Reply-to-Thrive from Teams (Phase 3): provider updates that ask the user a question can currently only be displayed.

Confirmed working since the last revision of this page: the AFTEC lookup end to end (unidata.reached: true, 55 ms — the Meraki route-table fix holds), group membership via the PS-PROXY worker, the initial-password handover, and set_manager.

  • Remove the Entra manager mappingset_manager is now proven live through the PS-PROXY worker (case 1029, 2026-08-10), which was the only precondition (psi-notify-bot#42, build-order step 14).
  • Redeploy the worker for set_password — PSI Portal can now generate a hire’s initial password and queue the on-prem set, but the service on PS-PROXY predates that task type, so the queue row would sit unclaimed until Install-PostTaskWorker.ps1 -Apply runs. See PSI Portal → Initial-password handover.
  • instance in /api/alerts/status reports “local” on Flex Consumption — cosmetic, but it makes per-instance matrix state unverifiable from outside.
  • Portal transport go-live — capture the create-case payload on the next real new-user request (scripts/thrive-session/capture-payload.js --submit), add psi-helpdesk--thrive-portal-user/--thrive-portal-password, CA exemption for PS-PROXY’s egress IP, install the session refresher, then set HELPDESK_TRANSPORT=portal.
  • CS4653129 cancelled itself — the 2026-07-28 portal test case flipped to Cancelled ~77 s after creation, cause unknown. Confirm a normally-worded case survives before trusting create-and-forget.
  • Sign off the remaining policy rows — the matrix is now editable in PSI Portal and seeded, but a rule IT has not confirmed still blocks automatic provisioning by design. Department 125’s shift-supervisor rule (8 on-prem groups) is signed; the rest are drafts.
  • Auto-file the 30/90-day termination reviews — the checklist states them; auto-creating the review case at the deadline needs ScheduleFunction integration.
  • Reply-to-Thrive from Teams (Phase 3) — provider updates that ask the user a question (“do you need us to do anything?”) can currently only be displayed; replying on the thread would reach Thrive.
  • OOTO org decision — confirm “silent” stays the default for departed mailboxes, or move customer-facing roles to auto-reply (offboarding-ooto-recommendations.md).
  • Phase 3 backlog: message action (“Create IT case” from any Teams message), screenshot/attachment ingestion, interactive manager card for position-change equipment, AlertEngine voice hook for “I can’t work” / line-down.

Done since launch: rename + green icon, org-wide setup policy, Thrive techsupport@ contact registration (their ServiceNow drops unknown senders), forwarder-model rework, mirror-card ack update, dept flavors, ProApps fields, HR A/R/C flows, forwarded-update dedupe/preview cleanup.