Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,76 @@ For multi-step tasks, state a brief plan:
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```

## 5. Project Architecture

**Flask + vanilla JS + D3. No ORM, no front-end framework.**

```
simple_org_chart/ ← Python package (Flask app)
app_main.py ← All Flask routes, filter parsers, API endpoints
msgraph.py ← Graph API helpers: fetch_all_employees, probe_graph_capabilities, etc.
reports.py ← Pure filter functions (apply_tagpicker_filters, apply_last_login_filters, …)
data_update.py ← Sync orchestration: token → probe caps → fetch employees → write caches
config.py ← Path constants (DATA_DIR, *_FILE paths)
settings.py ← load_settings / save_settings helpers
auth.py ← @require_auth decorator
static/
app.js ← D3 org chart, node click/drag, employee detail panel
reports.js ← Report filter UI, tagpicker/toggle renderers, REPORT_CONFIGS
reports.css ← Report filter layout styles
locales/en-US.json← All user-facing strings (i18n keys)
data/ ← JSON caches written at sync time (git-ignored)
```

## 6. Adding a Report Filter

Every filter touches all of these layers — miss one and it silently does nothing:

1. **`msgraph.py`** — add the field to `$select` in `fetch_all_employees` / `collect_last_login_records`; populate it on each record dict.
2. **`reports.py`** — add the parameter to every `apply_*_filters` function signature; implement the filtering logic.
3. **`app_main.py`** — parse the query-string param in `_parse_standard_toggle_args` or `_parse_tagpicker_args`; forward it to the filter function in every relevant route (GET + export).
4. **`static/reports.js`** — add the filter object to `_standardToggleFilters()` or `TAGPICKER_FILTERS`; add `requiredCapability` if the filter needs a Graph permission beyond `User.Read.All`.
5. **`static/locales/en-US.json`** — add label and (for tagpickers) placeholder/mode keys.

## 7. Graph Permission & Capability Gating

Filters that depend on Graph permissions beyond the base `User.Read.All` must be gated:

| Filter group | Required permission | Capability key |
|---|---|---|
| Mailbox type (User / Shared / Room) | `MailboxSettings.Read` | `mailbox_settings_read` |
| GAL visibility (Hidden / Visible) | `MailboxSettings.Read` | `mailbox_settings_read` |
| Inactivity day-range | `AuditLog.Read.All` + Entra P1/P2 | `audit_log_read_all` |

**How it works:**
- `msgraph.probe_graph_capabilities(token)` decodes the JWT access token's `roles` claim (no extra API calls) at the start of every sync and writes `data/graph_capabilities.json`.
- `GET /api/graph-capabilities` serves that file to the front end.
- `reports.js` fetches capabilities before first render; `_isFilterCapable(filter)` checks `filter.requiredCapability` against the loaded flags.
- Incapable filter buttons get `aria-disabled` + class `filter-chip--unavailable` (greyed out, tooltip explains missing permission).

When adding a new filter that needs a Graph permission:
1. Add a probe call in `probe_graph_capabilities()` if the capability isn't already detected.
2. Set `requiredCapability: '<key>'` on the filter object in `reports.js`.

## 8. Data Flow: Sync → Cache → API → UI

```
data_update.run_data_update()
├─ probe_graph_capabilities() → data/graph_capabilities.json
├─ fetch_all_employees() → data/employee_data.json, missing_manager, filtered_users, …
├─ collect_last_login_records()
│ └─ enrich with managerId from employee list
│ → data/last_login_records.json
└─ collect_disabled_users() → data/disabled_user_records.json

GET /api/reports/<type> → load cache → apply_*_filters() → JSON response
GET /api/graph-capabilities → data/graph_capabilities.json → JSON response
```

## 9. i18n Rules

- Every user-visible string must have a key in `static/locales/en-US.json`.
- The translator `t(key)` is available in `reports.js` via `getTranslator()`.
- Never hardcode English strings in JS templates.
- When adding filters: add `labelKey`, `placeholderKey`, `resetLabelKey`, `modeIncludeLabelKey`, `modeExcludeLabelKey` references and the matching JSON entries.
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ SimpleOrgChart is a Flask application backed by Azure Active Directory (Entra ID
- Hardened security defaults: strict Content Security Policy, sanitized redirects, login isolation, and placeholder-secret protection.
- Modular front end: no inline scripts or styles; shared CSS variables power `configure`, `reports`, and org chart experiences.
- Daily automation: background scheduler refreshes Azure AD data (20:00 local time) and persists JSON caches under `data/`.
- Admin reporting: missing managers, filtered users, and last-login inactivity insights—each with one-click XLSX export.
- Admin reporting: missing managers, filtered users, last-login inactivity, missing profile pictures, data quality issues, and recently hired users—each with one-click XLSX export.
- Export tooling: SVG/PNG/PDF org chart capture and server-backed XLSX generation for the current chart tree.
- Deployment ready: ships with Docker Compose and a Gunicorn configuration (`deploy/gunicorn.conf.py`) for containerized hosting.

Expand Down Expand Up @@ -39,8 +39,8 @@ SimpleOrgChart is a Flask application backed by Azure Active Directory (Entra ID
- `User.Read.All`
- `Presence.Read.All` *(enables live Teams presence status indicators on org chart cards)*
- `LicenseAssignment.Read.All` *(required for licensing insights and admin reports)*
- `AuditLog.Read.All` *(required for last sign-in metrics and disabled-user audit timestamps)*
- `MailboxSettings.Read` *(enables mailbox-type metadata used by last sign-in filters; without it, all mailboxes are treated as standard users)*
- `AuditLog.Read.All` *(required for last sign-in metrics and disabled-user audit timestamps; also enables the inactivity day-range filters on the Last Logins report — without it those filters are greyed out)*
- `MailboxSettings.Read` *(enables mailbox-type metadata used by mailbox-type filters; without it the User/Shared/Room mailbox filters are greyed out)*
- Grant admin consent for the tenant.

3. **Create a Client Secret**
Expand Down Expand Up @@ -125,7 +125,7 @@ docker compose up -d
```

- Default port: `APP_PORT` (defaults to `5000`). Override it in `.env` to change container and host bindings.
- Persistent data resides in the `orgchart_data` volume. Remove it to rebuild caches from scratch.
- Persistent data resides in the `./data` and `./config` bind-mounted directories. Remove their contents to rebuild caches from scratch.
- Local execution outside Docker is not supported; use the provided container workflow for development and production.

## Key Features
Expand All @@ -139,7 +139,12 @@ docker compose up -d
- Users by last sign-in activity
- Employees hired in the last 365 days
- Users hidden by filters
- Users without profile picture
- Users without a hiring date
- Users with data quality issues (whitespace, uppercase emails)
- User Scanner (OSINT) — individual and organization-wide email presence scans
- **Rich Report Filters**: Toggle groups (mailbox type, account status, license status, user type, GAL visibility, mailbox presence, manager presence) plus tagpicker filters for title, department, country, and state/province. Filters that require Graph permissions not currently granted are automatically greyed out with a tooltip explaining what is missing.
- **Permission-Gated Filters**: On each data sync the app probes which Graph API capabilities are available and persists the result. Filters that require `AuditLog.Read.All` (inactivity ranges), `MailboxSettings.Read` (mailbox type), or Exchange-backed `showInAddressList` (GAL visibility) are disabled in the UI until the corresponding permission is granted and a sync is run.
- **Automated Email Reports**: Schedule daily, weekly, or monthly reports sent via SMTP after data synchronization.
- **Export Options**: SVG/PNG/PDF snapshots and XLSX exports for reports and chart data.
- **Caching & Scheduling**: JSON caches regenerate nightly; manual refresh endpoints keep data current on demand.
Expand Down Expand Up @@ -291,12 +296,17 @@ Organization scans use file-based state (`data/full_scan_state.json`) and a canc

- `data/employee_data.json` – Full org hierarchy.
- `data/missing_manager_records.json` – Missing manager snapshot.
- `data/filtered_user_records.json` – Users hidden by org chart filters.
- `data/disabled_user_records.json` – Disabled users enriched with license and sign-in metadata.
- `data/last_login_records.json` – Active users with last sign-in timestamps.
- `data/recently_hired_employees.json` – Users hired in the last 365 days.
- `data/missing_photo_records.json` – Users without a profile picture.
- `data/missing_hire_date_records.json` – Users without a hiring date (`employeeHireDate`).
- `data/dirty_data_records.json` – Users with data quality issues (whitespace, uppercase emails).
- `data/graph_capabilities.json` – Graph API capability flags written on each sync; controls which report filters are enabled in the UI.
- `data/user_scanner_results.json` – Cached results from the most recent organization-wide OSINT scan.
- `data/user_scanner_history.json` – Last five scan run metadata entries.
- `data/user_scanner_exports/` – Downloaded XLSX workbooks for completed scans.
- Additional files exist for filtered/disabled-with-license/hiring reports.

If a cache is missing or stale, hit **Refresh Data** on the reports page or start the app with `RUN_INITIAL_UPDATE=true`.

Expand Down
Loading
Loading