Background Job — Slack Roster → app_users
(background process — no screen)
Background Job — Slack Roster Sync
Two halves, deliberately split because the app has no Slack credentials:
- Viktor-side:
skills/im2_app/scripts/roster_sync.pywrites the Slack directory into theslack_rostertable. - App-side:
POST /api/users/refresh(im2.py, admin only — the "Refresh from Slack directory" button on the Users screen) reconcilesslack_rosterintoapp_users.
Trigger / schedule
- Half 1 runs on the Viktor cron
/logistics/im2-nightly-sync, 1:00 AM CT (0 6 *UTC), together withqb_refresh.py --apply; silent unless it fails (source:skills/im2_app/SKILL.md). The script's own docstring says "Run nightly (Viktor cron)". Cron registration was not re-verified in this task — confirm withsdk.tools.scheduled_crons.list_crons. - Half 2 has no schedule at all — it only runs when an admin presses the button on the Users screen. Nothing propagates a new hire into
app_usersautomatically.
Inputs
coworker_list_slack_users(include_bots=False)— ~48 people. Anyone with no email is skipped.usernameis derived as the local part of the work email (email.split('@')[0]).- DSN:
/work/credentials/do/sa-apps-db.json→connection.uriwith/defaultdbreplaced by/im2.
Tables written (im2 database)
| Table | By | Change |
|---|---|---|
slack_roster (username PK, display_name, email, slack_id, synced_at) | roster_sync.py | create table if not exists, then truncate and full re-insert. |
app_users | /api/users/refresh | inserts missing people at role level3, active true; updates display_name/email and re-activates returners; sets active=false for anyone in app_users but no longer in the roster. Never changes an existing person's role. |
audit_log | /api/users/refresh | one row per add/deactivate: sku='USER:{name}', field_name='roster_sync', new_value = added / deactivated, changed_by = the admin, source='user_admin'. |
A manual add path exists for non-Slack people: POST /api/users/add (name + email + level).
External calls
Slack directory listing through the Viktor SDK (sdk.tools.slack_admin_tools). The droplet makes no Slack calls.
Failure behaviour / idempotency
- Half 1 is idempotent in effect but not atomic-safe by design: it truncates and re-inserts inside one transaction, so a crash mid-run rolls back and leaves the previous roster; but a successful run that returned a short Slack list would shrink the table — there is no minimum-row guard (unlike the vendor sync's
<100abort). - Half 2 is fully idempotent: pressing Refresh twice changes nothing the second time.
- Neither half retries.
Logs to look at
- Cron output for
/logistics/im2-nightly-sync(slack_roster: N people). - The JSON returned by
/api/users/refresh:{"ok":true,"added":[…],"deactivated":[…],"roster":N}. audit_log where field_name='roster_sync'.- Freshness:
select count(*), max(synced_at) from slack_roster;
Known limits / gaps
- A stale
slack_roster(cron failed) silently causes the Refresh button to deactivate real users — a truncated/short roster looks like "these people left Slack". No guard. - Deactivation is one-way from Slack membership; someone who keeps Slack but leaves the warehouse stays active.
- Role assignment is always manual after the first insert at
level3. usernamecollides if two people share an email local part across domains.- Half 2 never runs unattended, so the effective end-to-end sync is manual.