toga-ai 1.0.776 → 1.0.778
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/test/INDEX.md +1 -0
- package/knowledge/1.0/apps/test/architecture.md +5 -0
- package/knowledge/1.0/apps/test/features/github-audit-script.md +60 -0
- package/knowledge/1.0/apps/tools/INDEX.md +3 -2
- package/knowledge/1.0/apps/tools/architecture.md +14 -4
- package/knowledge/1.0/apps/tools/features/compass-user-persona-admin.md +157 -26
- package/knowledge/1.0/apps/tools/features/github-audit.md +185 -0
- package/knowledge/1.0/apps/tools/features/mvc-data-access-patterns.md +63 -4
- package/knowledge/1.0/standards/backend-php.md +11 -1
- package/knowledge/1.0/standards/frontend.md +24 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +58 -2
- package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +29 -1
- package/knowledge/2.0/apps/_underscore/features/record-change-audit-log.md +28 -6
- package/knowledge/2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md +103 -0
- package/knowledge/2.0/apps/api2/features/login-cross-client-user-resolution.md +15 -2
- package/knowledge/2.0/apps/api2/features/v2-rest-query-contract.md +39 -2
- package/knowledge/INDEX.md +4 -4
- package/knowledge/clients/compass-canada/profile.md +9 -0
- package/knowledge/clients/compass-usa/INDEX.md +1 -1
- package/knowledge/clients/compass-usa/features/cost-centers.md +44 -2
- package/knowledge/clients/compass-usa/features/people-file-user-lifecycle.md +41 -1
- package/knowledge/clients/nycdoe/INDEX.md +1 -0
- package/knowledge/clients/nycdoe/features/edi-850-receive-and-997-ack.md +108 -0
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +5 -2
- package/knowledge/clients/nycdoe/profile.md +2 -1
- package/knowledge/clients/rate/features/aig-contract-creation.md +130 -2
- package/knowledge/clients/rate/features/subscription-cancellation.md +52 -2
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Create Elastic Beanstalk Environment (script)](features/create-elastic-beanstalk.md) | `team/aws/create_elastic_beanstalk.php` is a **standalone** (no `App_` framework) constants-driven PHP generator. | test/team/aws/create_elastic_beanstalk.php |
|
|
10
10
|
| [Developer Generators (password, UUID)](features/dev-generators.md) | Two tiny **1.0 `App_` framework** convenience scripts for everyday developer needs. | test/team/generate_password.php, test/team/uuid.php |
|
|
11
11
|
| [Forecast vs NetSuite Discrepancy Analysis](features/forecast-netsuite-discrepancy-analysis.md) | `team/forecast-netsuite/discrepancy_analysis.php` detects discrepancies between our **Forecast database** and **NetSuite** (the source of truth for all sales da | test/team/forecast-netsuite/discrepancy_analysis.php |
|
|
12
|
+
| [GitHub Audit Script (team/github_audit.php)](features/github-audit-script.md) | `team/github_audit.php` is a **standalone** (no `App_` framework) browser/CLI script that reports lines added/removed, commits, unique authors, and active repos | test/team/github_audit.php |
|
|
12
13
|
| [@goagilant.com → @togatech.com Email-Domain Migration (1.0 + 2.0)](features/goagilant-to-togatech-email-migration.md) | Reference + technique for migrating the company email domain `@goagilant.com` → `@togatech.com` across **both** platforms. | migrate_goagilant_to_togatech_2026-06-26.sql, migrate_goagilant_to_togatech_LEGACY_2026-06-26.sql |
|
|
13
14
|
| [Static (no-DB) Regression Harness for 1.0 Logic + Source Drift Guard](features/static-no-db-regression-harness.md) | 1.0 has **no PHPUnit**, and most of its business logic sits inside methods that also write SQL, so "just call it" means standing up a client database. | test/@Mark/AIG/test_multi_email.php, test/@Mark/TRUE-80952/test_queued_email_attachments.php, library/app/database.php, library/app/api/toga2.php |
|
|
14
15
|
| [TableView Builder (2.0 TableViews SQL generator)](features/tableview-builder.md) | `team/tableViewBuilder/` generates SQL `INSERT` statements for the **2.0 `TableViews`**, `TableViewFields`, and `TableViewJoins` tables from a plain SQL `SELECT | test/team/tableViewBuilder/TableViewGenerator.php, test/team/tableViewBuilder/index.php, test/team/tableViewBuilder/Instructions.md |
|
|
@@ -22,6 +22,7 @@ related:
|
|
|
22
22
|
- ./features/dev-generators.md
|
|
23
23
|
- ./features/talos-kb-pipeline.md
|
|
24
24
|
- ./features/goagilant-to-togatech-email-migration.md
|
|
25
|
+
- ./features/github-audit-script.md
|
|
25
26
|
---
|
|
26
27
|
|
|
27
28
|
## Summary
|
|
@@ -85,6 +86,7 @@ manually against the target database; they do not execute changes themselves.
|
|
|
85
86
|
| `generate_password.php`, `uuid.php` | 1.0 | dev utility | [dev-generators](./features/dev-generators.md) |
|
|
86
87
|
| `talos/kb_uploader.php`, `talos/kb_processor.php` | standalone | S3 + AWS Bedrock KBs | [talos-kb-pipeline](./features/talos-kb-pipeline.md) |
|
|
87
88
|
| `migrate_goagilant_to_togatech_*.sql` | standalone | 2.0 `Client_` DBs + 1.0 schemas | [goagilant-to-togatech-email-migration](./features/goagilant-to-togatech-email-migration.md) |
|
|
89
|
+
| `github_audit.php` | standalone | GitHub org (agilantsolutions) | [github-audit-script](./features/github-audit-script.md) |
|
|
88
90
|
|
|
89
91
|
**Retired / not documented:** `team/DOA_gitbook/` is being retired.
|
|
90
92
|
|
|
@@ -95,3 +97,6 @@ manually against the target database; they do not execute changes themselves.
|
|
|
95
97
|
processor in [talos-kb-pipeline](./features/talos-kb-pipeline.md).
|
|
96
98
|
- 2026-06-26 — Documented the @goagilant.com → @togatech.com email-domain migration SQL
|
|
97
99
|
artifacts in [goagilant-to-togatech-email-migration](./features/goagilant-to-togatech-email-migration.md).
|
|
100
|
+
- 2026-09-04 — Documented `team/github_audit.php`, the standalone monthly GitHub org
|
|
101
|
+
code-churn audit (later ported into Tools) in
|
|
102
|
+
[github-audit-script](./features/github-audit-script.md).
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: GitHub Audit Script (team/github_audit.php)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: test
|
|
5
|
+
project: Test
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- test/team/github_audit.php
|
|
13
|
+
related:
|
|
14
|
+
- ../architecture.md
|
|
15
|
+
- ../../tools/features/github-audit.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Summary
|
|
19
|
+
|
|
20
|
+
`team/github_audit.php` is a **standalone** (no `App_` framework) browser/CLI script that
|
|
21
|
+
reports lines added/removed, commits, unique authors, and active repos across the
|
|
22
|
+
`agilantsolutions` GitHub org for one calendar month. Quick, run-it-locally monthly audit.
|
|
23
|
+
|
|
24
|
+
This is the **original build** that was then ported into the Tools app as
|
|
25
|
+
[`/developers/github-audit`](../../tools/features/github-audit.md). The app version is the
|
|
26
|
+
maintained one; this script is the lightweight local runner.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
1. **Config** via `const` at the top of the file: `AUDIT_MONTH` (`YYYY-MM`) and
|
|
31
|
+
`GITHUB_ORG`.
|
|
32
|
+
2. **Token** is read from the `GITHUB_AUDIT_TOKEN` **environment variable** — never
|
|
33
|
+
hardcoded, so it can't be committed.
|
|
34
|
+
3. Same **per-commit exact counting** as the app tool: list each commit in the month window,
|
|
35
|
+
fetch each commit's stats, skip merge commits and forks.
|
|
36
|
+
4. Same **reliability**: `202` "stats not ready" retry, secondary rate-limit retry, and a
|
|
37
|
+
primary rate-limit floor that stops the run.
|
|
38
|
+
5. Same **month-range validation** (`^\d{4}-\d{2}$` plus month number 01–12, so `2026-99`
|
|
39
|
+
is rejected rather than silently overflowed by `DateTime`).
|
|
40
|
+
|
|
41
|
+
Fits the `test` repo's documented **`team/` scripts** surface (see architecture).
|
|
42
|
+
|
|
43
|
+
## Gotchas
|
|
44
|
+
|
|
45
|
+
- **Token must be in the environment.** With `GITHUB_AUDIT_TOKEN` unset the run fails — set
|
|
46
|
+
it before running; do not paste a PAT into the file.
|
|
47
|
+
- **Shared org rate limit.** A big audit consumes the org's GitHub budget; avoid running it
|
|
48
|
+
in parallel with the Tools GitHub Audit or the Design tool (same org token pool).
|
|
49
|
+
|
|
50
|
+
## Change history
|
|
51
|
+
|
|
52
|
+
- 2026-09-04 — Created. Standalone monthly org code-churn audit script; token from
|
|
53
|
+
`GITHUB_AUDIT_TOKEN`. Later ported into the Tools app as `/developers/github-audit`.
|
|
54
|
+
(jcardinal)
|
|
55
|
+
|
|
56
|
+
## Related docs
|
|
57
|
+
|
|
58
|
+
- [Test Architecture](../architecture.md)
|
|
59
|
+
- [Tools GitHub Audit (app port)](../../tools/features/github-audit.md)
|
|
60
|
+
</content>
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
| [/clickup/aliases — ClickUp Label-Alias Admin (Tools → 2.0 Team schema)](features/clickup-label-aliases-admin.md) | The human side of the NetSuite→ClickUp **Stakeholders / End Customer** labels. | tools/mvc/clickup/aliases/get.php, tools/mvc/clickup/aliases/post.php, tools/_/app/clickup/aliases.php, tools/_/app/nav.php |
|
|
7
7
|
| [Developer Dashboard (ClickUp Sprint, tools /developer)](features/clickup-sprint-dashboard.md) | A native **ClickUp sprint dashboard** in the 1.0 `tools` app at route `/developer` (the **"Developer Dashboard"**, renamed from `/clickup/react` / "Clickup" — s | tools/_/app/clickup/sprint.php, tools/v2/sprints/tile/index.php, tools/v2/sprints/current/index.php, tools/v2/sprints/status-breakdown/index.php, tools/v2/sprints/worktype-breakdown/index.php, tools/v2/sprints/points-by-dev/index.php, tools/v2/sprints/burndown/index.php, tools/assets/clickup/sprint-dashboard.html, tools/mvc/developer/get.php, tools/mvc/login/get.php, tools/_/app/nav.php |
|
|
8
8
|
| [CloudFront Client Setup](features/cloudfront-client-setup.md) | An SSO-gated admin tool at **`/devops/cloudfront-clients`** in the Tools 1.0 app that onboards a client onto **CloudFront + Route 53 across multiple AWS account | tools/_/app/devops/cloudfront.php, tools/mvc/devops/cloudfront-clients/get.php, tools/mvc/devops/cloudfront-clients/post.php, tools/assets/js/cloudfront-clients.js, tools/assets/css/cloudfront-clients.css, tools/_/app/nav.php, tools/_/app/frameworkindex.php, tools/config.production.ini |
|
|
9
|
-
| [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the
|
|
9
|
+
| [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass requests that used to mean "email a developer, who writes a `dbchang | tools/_/app/compass.php, tools/mvc/compass/users/get.php, tools/mvc/compass/users/post.php, tools/mvc/compass/personas/get.php, tools/mvc/compass/personas/post.php, tools/_/app/nav.php, tools/config.production.ini |
|
|
10
10
|
| [Design Demo Admin](features/design-demo-admin.md) | A self-serve admin UI at **`/design`** in the SSO-protected **Tools** app that lets the design team publish self-contained "Claude Design" HTML exports as **ver | tools/_/app/design/github.php, tools/mvc/design/get.php, tools/mvc/design/post.php, tools/assets/css/design.css, tools/assets/js/design.js, tools/_/app/frameworkindex.php, tools/_/app/nav.php, tools/composer.json |
|
|
11
11
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php, tools/mvc/developers/password/post.php |
|
|
12
12
|
| [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only triage/curation screen for the 2.0 Issue/Event error-reporting pipeline, built as a 1.0 Tools MVC page reading the **shared Core Logs DB** through | tools/mvc/errors/get.php, tools/mvc/errors/post.php, tools/mvc/errors/issue/get.php, tools/assets/css/style.css, tools/_/app/nav.php, tools/config.production.ini |
|
|
13
|
+
| [GitHub Audit (monthly code-churn report)](features/github-audit.md) | An internal report at **`/developers/github-audit`** (Developers nav group, personas `['Development Team','TOGa Technology']`) that shows, for one calendar mont | tools/_/app/github/audit.php, tools/mvc/developers/github-audit/get.php, tools/mvc/developers/github-audit/post.php, tools/_/app/nav.php |
|
|
13
14
|
| [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | An SSO-gated admin tool at **`/email-migration/notify`** (nav group **Email Migration** > **Legacy Notifier**, personas `['TOGa Technology','Development Team']` | tools/mvc/email-migration/notify/get.php, tools/mvc/email-migration/notify/post.php, tools/_/app/nav.php |
|
|
14
|
-
| [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php, library/app/error.php, library/app/database.php |
|
|
15
|
+
| [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php, library/app/error.php, library/app/database.php, library/app/string.php |
|
|
15
16
|
| [OneUptime Monitor Status Panel & Outage Alerting (tools /developer)](features/oneuptime-monitor-status-panel.md) | A live **operational monitor column** on the wall-display dashboard at `/developer` (the route was renamed from `/clickup/react` — the internal asset path `asse | tools/assets/clickup/sprint-dashboard.html, tools/_/app/clickup/monitors.php, tools/v2/monitors/status/index.php, tools/mvc/developer/get.php |
|
|
16
17
|
| [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
|
|
17
18
|
| [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini, tools/config.local.ini |
|
|
@@ -32,6 +32,7 @@ related:
|
|
|
32
32
|
- ./features/persona-gated-navigation.md
|
|
33
33
|
- ./features/developer-tools.md
|
|
34
34
|
- ./features/oneuptime-monitor-status-panel.md
|
|
35
|
+
- ./features/github-audit.md
|
|
35
36
|
- ../library/architecture.md
|
|
36
37
|
- ../library/features/mvc-page-pattern-and-app-skeleton.md
|
|
37
38
|
---
|
|
@@ -42,10 +43,12 @@ related:
|
|
|
42
43
|
simple interfaces, gated by Client_True staff persona. It is modeled on `togaview` and depends on
|
|
43
44
|
the `library` core. Auth comes via the SAML gateway `?saml=` handoff; the app owns its own
|
|
44
45
|
session and reads `Client_True` **read-only** — but it is **no longer a read-only app**: as of
|
|
45
|
-
PR tools#13 (
|
|
46
|
-
`Client_CompassCanada`) through
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
PR tools#13 (**merged to `_production` 2026-09-04**) it **WRITES** to client tenant databases
|
|
47
|
+
(`Client_Compass` / `Client_CompassCanada`) through the `[database_compass]` /
|
|
48
|
+
`[database_compasscanada]` ini sections, and PR tools#14 (**open**) adds writes to `Users`
|
|
49
|
+
identity fields (email, phone, cost center) plus staff-actor audit rows in `Logs_Compass` /
|
|
50
|
+
`Logs_CompassCanada` via two more ini sections — all four reusing the `[database_true]` account.
|
|
51
|
+
That is the first non-read-only surface in Tools. Adding a tool = create `mvc/<route>/get.php`
|
|
49
52
|
(copy `mvc/_TEMPLATE`) + add one `App_Nav::definition()` entry + `App_Auth::requireAuth([...])`.
|
|
50
53
|
|
|
51
54
|
**Critical rules:** Do NOT use bare `<header>` elements in this app — they inherit the legacy
|
|
@@ -68,6 +71,13 @@ persona list (`['Development Team']`) — never `App_Nav::personasForRoute()`**,
|
|
|
68
71
|
UNION of the folder's personas and the action's personas and so widens the grant straight back to
|
|
69
72
|
everyone who can see the nav group. Also: **persona checks read a 4-day session snapshot**, so
|
|
70
73
|
removing someone's persona does **not** revoke their access — their session must be killed.
|
|
74
|
+
**`App_Database` transactions are GLOBAL** — `beginTransaction()` / `commitTransaction()` /
|
|
75
|
+
`rollbackTransaction()` take no arguments and act on every registered connection, so an audit/log
|
|
76
|
+
row on another connection must be written **before** `beginTransaction()` or it is rolled back with
|
|
77
|
+
the work, and a bare `rollbackTransaction()` leaves autocommit off for the rest of the request
|
|
78
|
+
(silently dropping every later write). **A staff-editable `Users.email` is a login/reset identity**
|
|
79
|
+
— never ship one without a `Core.ClientEmailDomains` domain check and a cross-row duplicate
|
|
80
|
+
refusal, because no Compass tenant has a unique key on `email`.
|
|
71
81
|
|
|
72
82
|
## Boot & structure
|
|
73
83
|
|
|
@@ -5,7 +5,7 @@ repo: tools
|
|
|
5
5
|
project: Tools
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
|
-
status:
|
|
8
|
+
status: active
|
|
9
9
|
updated: 2026-09-04
|
|
10
10
|
owners: [ajean]
|
|
11
11
|
files:
|
|
@@ -22,31 +22,40 @@ related:
|
|
|
22
22
|
- ./saml-sso-auth.md
|
|
23
23
|
- ../architecture.md
|
|
24
24
|
- ../../../2.0/apps/_underscore/features/effective-persona-resolution.md
|
|
25
|
+
- ../../../2.0/apps/_underscore/features/record-change-audit-log.md
|
|
26
|
+
- ../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md
|
|
25
27
|
- ../../../clients/compass-usa/features/people-file-user-lifecycle.md
|
|
26
28
|
- ../../../clients/compass-usa/features/persona-model-and-levy-gating.md
|
|
29
|
+
- ../../../clients/compass-usa/features/cost-centers.md
|
|
27
30
|
- ../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
|
|
28
31
|
---
|
|
29
32
|
|
|
30
33
|
## Summary
|
|
31
34
|
|
|
32
|
-
Two pages in the Tools `developers` nav folder that let staff do, in a browser, the
|
|
35
|
+
Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass
|
|
33
36
|
requests that used to mean "email a developer, who writes a `dbchanges2` SQL file": **activate /
|
|
34
|
-
deactivate a Compass user**,
|
|
35
|
-
switcher — **US**
|
|
37
|
+
deactivate a Compass user**, **grant a persona**, and (as of 2026-09-04) **fix a user's email,
|
|
38
|
+
phone number and cost center**. Both tenants are covered by one tenant switcher — **US**
|
|
39
|
+
(`Client_Compass`) and **Canada** (`Client_CompassCanada`).
|
|
36
40
|
|
|
37
41
|
**This is the first thing in `tools` that WRITES to a client tenant database.** Until now the app
|
|
38
42
|
read `Client_True` read-only. That single fact drives most of the design and every open risk below.
|
|
39
43
|
|
|
40
|
-
Status:
|
|
44
|
+
Status: the two pages shipped on PR **tools#13**, now **merged to `tools/_production`**. The
|
|
45
|
+
field-editing work (email / phone / cost center) is PR **tools#14**, branch
|
|
46
|
+
`feature/compass-user-field-edits`, base `_production` — **open, not merged, not deployed.**
|
|
41
47
|
|
|
42
48
|
## Key files / entry points
|
|
43
49
|
|
|
44
50
|
- **`tools/_/app/compass.php`** — `App_Compass`, all data access for both pages (search, persona
|
|
45
|
-
resolution, activate/deactivate, grant).
|
|
46
|
-
- **`tools/mvc/compass/users/{get,post}.php`** — user search + activate/deactivate + grant persona
|
|
51
|
+
resolution, activate/deactivate, grant, and the three field setters).
|
|
52
|
+
- **`tools/mvc/compass/users/{get,post}.php`** — user search + activate/deactivate + grant persona
|
|
53
|
+
+ edit email / phone / cost center.
|
|
47
54
|
- **`tools/mvc/compass/personas/{get,post}.php`** — inspect one persona and who reaches it.
|
|
48
55
|
- **`tools/_/app/nav.php`** — the two nav entries, inside the existing `developers` folder.
|
|
49
|
-
- **`tools/config.production.ini`** —
|
|
56
|
+
- **`tools/config.production.ini`** — `[database_compass]` / `[database_compasscanada]`, plus the
|
|
57
|
+
two logs sections `[database_compasslogs]` (`Logs_Compass`) and `[database_compasscanadalogs]`
|
|
58
|
+
(`Logs_CompassCanada`).
|
|
50
59
|
|
|
51
60
|
## How it works
|
|
52
61
|
|
|
@@ -57,6 +66,7 @@ Status: built on PR **tools#13**, **not yet merged**. `status: draft` until it s
|
|
|
57
66
|
3. **See the personas that user reaches, and WHY** — direct row, own location, or sector.
|
|
58
67
|
4. **Inspect a persona** and list who reaches it, from all three sources.
|
|
59
68
|
5. **Grant a persona** — one `INSERT` into `Users_Personas`.
|
|
69
|
+
6. **Edit email, phone number and cost center** (2026-09-04, PR tools#14).
|
|
60
70
|
|
|
61
71
|
The "why" column is the point of the tool. Answering "who has persona N" from `Users_Personas`
|
|
62
72
|
alone badly under-reports on Compass, because most users hold their catalogue through their
|
|
@@ -65,6 +75,40 @@ framework's own — see
|
|
|
65
75
|
[Effective persona resolution](../../../2.0/apps/_underscore/features/effective-persona-resolution.md),
|
|
66
76
|
which also carries the measured prod numbers and the "reverse lookups must be set-based" rule.
|
|
67
77
|
|
|
78
|
+
### The three field edits (PR tools#14)
|
|
79
|
+
|
|
80
|
+
`App_Compass` gained `setEmail()`, `setPhone()`, `setCostCenter()`, the cost-center helpers
|
|
81
|
+
`searchCostCenters()`, `costCenterLocation()`, `personasForLocation()`, `collapsePersonaRows()`,
|
|
82
|
+
the audit writer `logChange()`, and the validators `normaliseEmail()`, `normalisePhone()`,
|
|
83
|
+
`emailDomainIsRegistered()`, `usersWithEmail()`, `isCrossRegionProtectedEmail()`.
|
|
84
|
+
|
|
85
|
+
**No schema change.** Each edit writes the `Users` row **and** the matching primary `Contacts`
|
|
86
|
+
child row inside **one transaction** — the same mirror the nightly importer maintains (see the
|
|
87
|
+
Contacts-mirror gotcha in
|
|
88
|
+
[PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md)).
|
|
89
|
+
|
|
90
|
+
**A staff-editable `Users.email` is a security-sensitive control, not a cosmetic field** — it is
|
|
91
|
+
the key the 2.0 forgot-password flow and login resolve on. Read
|
|
92
|
+
[User email as identity & the password-reset chain](../../../2.0/apps/_underscore/features/user-email-as-identity-and-password-reset.md)
|
|
93
|
+
before touching this code. These are the rails the email edit shipped with, and they are the
|
|
94
|
+
conditions of the decision below — **do not remove one without re-opening it**:
|
|
95
|
+
|
|
96
|
+
1. The address must be **typed twice** and match.
|
|
97
|
+
2. Its **domain must exist in `Core.ClientEmailDomains`** (`emailDomainIsRegistered()`).
|
|
98
|
+
3. The edit is **refused if any other `Users` row in that tenant already holds the address**
|
|
99
|
+
(`usersWithEmail()`) — the database will not stop you, see the gotcha below.
|
|
100
|
+
4. The edit is **refused until the operator confirms** when the target row has duplicate twins
|
|
101
|
+
(the same human on two `Users` rows).
|
|
102
|
+
5. The four `ensureCrossRegionUsersActive()` addresses are **blocked as both source and target**
|
|
103
|
+
(`isCrossRegionProtectedEmail()`), because that hardcoded worker2 list matches on email.
|
|
104
|
+
6. The **audit row is written before the write**, on its own connection (see the transaction
|
|
105
|
+
gotcha below).
|
|
106
|
+
|
|
107
|
+
Cost center is a **picker over distinct codes**, never a type-ahead (the table is ~910k active Unit
|
|
108
|
+
rows — see [Cost Centers](../../../clients/compass-usa/features/cost-centers.md)), and it forces a
|
|
109
|
+
**before/after persona diff** to be shown and confirmed, because a location change moves the user's
|
|
110
|
+
sector and therefore their catalogue.
|
|
111
|
+
|
|
68
112
|
### Access gating is deliberately tighter than the folder
|
|
69
113
|
|
|
70
114
|
Both pages `requireAuth(['Development Team'])` — narrower than the `developers` folder's own
|
|
@@ -72,16 +116,35 @@ persona set. That is on purpose: these are tenant **writes**, not read-only deve
|
|
|
72
116
|
Passing `App_Nav::personasForRoute()` here would widen the grant back to the folder's set; see the
|
|
73
117
|
nav-narrowing gotcha in [MVC data access patterns](./mvc-data-access-patterns.md).
|
|
74
118
|
|
|
119
|
+
### Audit trail — the tenant Logs DB, no schema change
|
|
120
|
+
|
|
121
|
+
The "no staff-actor audit trail exists" open item is **resolved**. A staff-actor row **can** be
|
|
122
|
+
written to `Logs_<Tenant>.Record` with no schema change, because `Record.userId` is **nullable**:
|
|
123
|
+
|
|
124
|
+
- `recordId` = **2** (`Core.Records` id for `Users`), `primaryKeyId` = the target `Users.id`,
|
|
125
|
+
`clientId` = the tenant's `Core.Clients` id, `userId` = **NULL**, staff identity in **`note`**.
|
|
126
|
+
- This is the shape `_Model_Compass_ApprovalDecision`
|
|
127
|
+
(`_underscore/Model/Compass/ApprovalDecision.php:1344-1366`) already hand-writes, so it is an
|
|
128
|
+
established pattern, not a new invention.
|
|
129
|
+
- **`Record.note` is `varchar(255)` = 255 CHARACTERS.** Truncate with **`mb_substr`**, never
|
|
130
|
+
`substr` — byte truncation of a 262-character note produced 155 bytes of invalid UTF-8 in a live
|
|
131
|
+
test.
|
|
132
|
+
|
|
133
|
+
That is why `[database_compasslogs]` and `[database_compasscanadalogs]` exist. Details and the
|
|
134
|
+
cross-cluster rules: [Record Change Audit
|
|
135
|
+
Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md).
|
|
136
|
+
|
|
75
137
|
### Database connections
|
|
76
138
|
|
|
77
|
-
|
|
78
|
-
**existing `[database_true]` account** — same client cluster, only `dbname` differs.
|
|
79
|
-
value was introduced.** See the accepted risk below.
|
|
139
|
+
The `[database_compass]` / `[database_compasscanada]` sections, and the two logs sections, all
|
|
140
|
+
reuse the **existing `[database_true]` account** — same client cluster, only `dbname` differs.
|
|
141
|
+
**No new secret value was introduced.** See the accepted risk below.
|
|
80
142
|
|
|
81
143
|
### Schema footprint: none
|
|
82
144
|
|
|
83
|
-
The tool creates and alters **nothing**. It reads/writes only `Users`, `
|
|
84
|
-
`Locations_Personas`, `Locations`,
|
|
145
|
+
The tool creates and alters **nothing**. It reads/writes only `Users`, `Contacts` (+ its primary
|
|
146
|
+
email/phone children), `Users_Personas`, `Locations_Personas`, `Locations`, reads `Personas` for
|
|
147
|
+
names, reads `Core.ClientEmailDomains` / `Core.Clients`, and inserts into `Logs_<Tenant>.Record`.
|
|
85
148
|
|
|
86
149
|
## Decisions
|
|
87
150
|
|
|
@@ -92,6 +155,19 @@ The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Pe
|
|
|
92
155
|
and then **closed unmerged** — do not resurrect them without re-opening that decision.
|
|
93
156
|
- **2026-09-04 — Reuse the `[database_true]` credential** rather than split out a least-privilege
|
|
94
157
|
Compass account. Raised twice in security review and consciously declined for now (see below).
|
|
158
|
+
- **2026-09-04 — Ship all three fields (email, phone, cost center), not phone-only (ajean).** The
|
|
159
|
+
independent architecture and security reviews both recommended shipping **phone only** and
|
|
160
|
+
deferring email. The developer was shown both and chose to ship all three **with the six safety
|
|
161
|
+
rails listed above**. Those rails are the conditions the decision was made under: removing the
|
|
162
|
+
domain allowlist, the cross-row duplicate refusal, the typed-twice confirmation, or the
|
|
163
|
+
audit-before-write re-opens the decision and needs a fresh security review.
|
|
164
|
+
- **2026-09-04 — Cost-center edits are deliberately TEMPORARY (ajean).** `setCostCenter()` writes
|
|
165
|
+
`Users.locationId` and `Users.c_erpEntityId` but **not** `Users.c_erpSystemEntityId`, so for any
|
|
166
|
+
user in the nightly PEOPLE file the change is undone that night when the importer re-resolves
|
|
167
|
+
`locationId` from the employee key. Chosen over corrupting the employee directory key —
|
|
168
|
+
`c_erpSystemEntityId` is also copied into `c_hrEmpDirectoryKey` and treated as unique per
|
|
169
|
+
employee. Same one-way-durability shape as the activation gotcha. Detail:
|
|
170
|
+
[Cost Centers](../../../clients/compass-usa/features/cost-centers.md).
|
|
95
171
|
|
|
96
172
|
## Gotchas
|
|
97
173
|
|
|
@@ -100,34 +176,89 @@ The tool creates and alters **nothing**. It reads/writes only `Users`, `Users_Pe
|
|
|
100
176
|
absent from that day's file — so an activation only sticks if the person is actually in the file.
|
|
101
177
|
Deactivation holds, because the cron never reactivates anybody. Full lifecycle:
|
|
102
178
|
[PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
|
|
179
|
+
- **⚠ A cost-center edit is undone the same night for anyone in the PEOPLE file** — by design, see
|
|
180
|
+
the decision above. Set expectations with the requester; it is a stop-gap, not a fix.
|
|
181
|
+
- **⚠ The database will silently accept a duplicate email — the duplicate check MUST live in code.**
|
|
182
|
+
Prod-verified 2026-09-04: **neither** `Client_Compass.Users` nor `Client_CompassCanada.Users` has
|
|
183
|
+
a `UNIQUE KEY` on `email` (nor any index on it). The blank-client template's `UNIQUE KEY email`
|
|
184
|
+
does not describe these tenants. `usersWithEmail()` is the only thing standing between a typo and
|
|
185
|
+
two accounts sharing one login identity. Full index list:
|
|
186
|
+
[PEOPLE-file User Lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
|
|
187
|
+
- **⚠ A per-tenant email-domain allowlist must span BOTH Compass clients.** Real Compass Canada
|
|
188
|
+
users are on `compass-canada.com`, which is registered in `Core.ClientEmailDomains` to
|
|
189
|
+
**client 2 (Compass Group / `Compass_Usa`)**, not client 43. Scoping the domain check to the
|
|
190
|
+
tenant being edited rejects legitimate Canadian staff. `Users.clientId` is also **NULL** on
|
|
191
|
+
~178,615 of 178,616 `Client_Compass` rows, so the client id cannot be derived from the user row.
|
|
192
|
+
See [Compass Canada profile](../../../clients/compass-canada/profile.md).
|
|
103
193
|
- **A cross-region user still needs a code change and a deploy.** The only durable way to hold one
|
|
104
194
|
active is the hardcoded four-address list in `ensureCrossRegionUsersActive()` in
|
|
105
|
-
`worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that
|
|
195
|
+
`worker2/Worker/Client/Compass/PeopleFile.php`. This tool does not change that — and because that
|
|
196
|
+
list matches on **email**, those four addresses are blocked from this tool's email edit entirely.
|
|
106
197
|
- **Every user search is a full table scan.** `Client_Compass.Users` has no index on `email`,
|
|
107
198
|
`c_hrEmpUsername`, `c_hrEmpPersonnelNbr` or `isActive` (176k rows; `EXPLAIN` reports `type=ALL`,
|
|
108
199
|
no possible keys). Same on `Client_CompassCanada`. Keep result sets bounded and do not add
|
|
109
|
-
per-row lookups against those columns in a loop.
|
|
200
|
+
per-row lookups against those columns in a loop. The cost-center search is the same story —
|
|
201
|
+
`Locations` has no usable index for it either, which is why it is a picker, not a type-ahead.
|
|
110
202
|
- **⚠ Accepted risk — the tool widens the blast radius of one shared credential.** It points the
|
|
111
|
-
`[database_true]` account at
|
|
112
|
-
web-readable until the 2026-07-24 `FilesMatch` deny blocks, so **rotation of
|
|
113
|
-
|
|
114
|
-
|
|
203
|
+
`[database_true]` account at four more schemas (two tenants + two logs). `config.production.ini`
|
|
204
|
+
is committed and was web-readable until the 2026-07-24 `FilesMatch` deny blocks, so **rotation of
|
|
205
|
+
every secret in that file is still owed**. A least-privilege Compass account would shrink the
|
|
206
|
+
blast radius of that shared credential, which also reaches `Client_True` — where the
|
|
207
|
+
`Development Team` persona rows that gate this very tool live. An inline comment in the ini
|
|
208
|
+
records this and lists the exact grants a split-out account would need. Never paste the value
|
|
209
|
+
anywhere.
|
|
115
210
|
- **⚠ Persona revocation does not take effect for up to 4 days.** Tools snapshots persona names into
|
|
116
211
|
`$_SESSION` at login and sessions last 4 days, so removing someone's `Development Team` persona
|
|
117
212
|
does not lock them out of these pages until their session dies. See
|
|
118
|
-
[SAML SSO & persona-gated auth](./saml-sso-auth.md).
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
213
|
+
[SAML SSO & persona-gated auth](./saml-sso-auth.md). Now that these pages write user identity
|
|
214
|
+
fields, a **narrower persona for the write actions** is worth considering.
|
|
215
|
+
- **⚠ `App_Database` transactions are GLOBAL, so the audit write must happen BEFORE
|
|
216
|
+
`beginTransaction()`.** `beginTransaction()` / `commitTransaction()` / `rollbackTransaction()`
|
|
217
|
+
take no arguments and act on **every** registered connection. A `Logs_<Tenant>` insert issued
|
|
218
|
+
inside the transaction belongs to it and disappears on rollback — exactly when you most want the
|
|
219
|
+
audit row. A bare `rollbackTransaction()` also leaves autocommit off for the rest of the request.
|
|
220
|
+
Full footgun: [MVC data access patterns](./mvc-data-access-patterns.md).
|
|
125
221
|
- **Detecting a no-op UPDATE needs `App_Database::affectedRows()`** — `App_Database::query()` returns
|
|
126
222
|
the `App_Query` execute result, not a row count. Details in
|
|
127
223
|
[MVC data access patterns](./mvc-data-access-patterns.md).
|
|
224
|
+
- **Strip line breaks out of anything you log or email.** `App_Page::getVarIfSet()` does **not**
|
|
225
|
+
remove newlines, so a request value can forge extra `error_log()` lines. Use
|
|
226
|
+
`App_String::stripBreaksAndTabs()`. It matters for the email field too: a stray line break or
|
|
227
|
+
degree sign survives the filter, stores fine, escapes fine, and then makes PHPMailer silently
|
|
228
|
+
drop the recipient. See [MVC data access patterns](./mvc-data-access-patterns.md).
|
|
229
|
+
|
|
230
|
+
## Open follow-ups (other repos)
|
|
231
|
+
|
|
232
|
+
- **dbchanges2** — add index `Locations (locationTypeId, isActive, c_erpSystemEntityId)` in **both**
|
|
233
|
+
Compass tenants; the cost-center search currently reads the whole table.
|
|
234
|
+
- **_underscore** — fix `_Model_Compass_SalesOrder::_costCenter()`
|
|
235
|
+
(`_underscore/Model/Compass/SalesOrder.php:287-305`): it joins
|
|
236
|
+
`INNER JOIN Contacts ON Contacts.id = Users.id`, which should be `Users.contactId`.
|
|
237
|
+
- **worker2** — scope `ensureCrossRegionUsersActive()` to a `Users.id` list instead of matching on
|
|
238
|
+
email; that would also lift this tool's block on those four addresses.
|
|
239
|
+
- **ops** — rotate every secret in `tools/config.production.ini`; consider a least-privilege
|
|
240
|
+
Compass account.
|
|
128
241
|
|
|
129
242
|
## Change history
|
|
130
243
|
|
|
244
|
+
- 2026-09-04 — Added **email, phone and cost-center editing** (PR tools#14,
|
|
245
|
+
`feature/compass-user-field-edits`, **open / not merged / not deployed**): new `App_Compass`
|
|
246
|
+
setters plus the `normaliseEmail` / `normalisePhone` / `emailDomainIsRegistered` /
|
|
247
|
+
`usersWithEmail` / `isCrossRegionProtectedEmail` validators, a cost-center picker with a mandatory
|
|
248
|
+
before/after persona diff, and a `Users` + primary `Contacts` write in one transaction. No schema
|
|
249
|
+
change. Recorded the decision to ship **all three** fields against independent
|
|
250
|
+
architecture/security advice to defer email, and that the six safety rails (typed twice,
|
|
251
|
+
`Core.ClientEmailDomains` domain check, cross-row duplicate refusal, duplicate-twin confirmation,
|
|
252
|
+
cross-region address block, audit-before-write) are the conditions of that decision. Recorded that
|
|
253
|
+
**cost-center edits are deliberately temporary** because `c_erpSystemEntityId` is not overwritten.
|
|
254
|
+
**Resolved the "no staff-actor audit trail" open item** — `Logs_<Tenant>.Record.userId` is
|
|
255
|
+
nullable, so `recordId = 2` (`Users`) + `primaryKeyId` + staff identity in `note` works with no
|
|
256
|
+
schema change (`note` is 255 **characters** — truncate with `mb_substr`); hence the new
|
|
257
|
+
`[database_compasslogs]` / `[database_compasscanadalogs]` ini sections, still on the existing
|
|
258
|
+
`[database_true]` account. Prod-verified that **neither tenant has a `UNIQUE KEY` on
|
|
259
|
+
`Users.email`**, so the duplicate check must live in code, and that `compass-canada.com` is
|
|
260
|
+
registered to Compass **USA** (client 2), so an email-domain allowlist must span both Compass
|
|
261
|
+
clients. Marked PR tools#13 **merged** and the doc `active`. (ajean)
|
|
131
262
|
- 2026-09-04 — Initial: built the two-page Compass user/persona admin (PR tools#13, unmerged) — the
|
|
132
263
|
first `tools` pages that write to a client tenant DB, gated to `Development Team` only, with a
|
|
133
264
|
US/Canada switcher and new `[database_compass]` / `[database_compasscanada]` ini sections reusing
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: GitHub Audit (monthly code-churn report)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-04
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/github/audit.php
|
|
13
|
+
- tools/mvc/developers/github-audit/get.php
|
|
14
|
+
- tools/mvc/developers/github-audit/post.php
|
|
15
|
+
- tools/_/app/nav.php
|
|
16
|
+
related:
|
|
17
|
+
- ../architecture.md
|
|
18
|
+
- ./design-demo-admin.md
|
|
19
|
+
- ../../../standards/frontend.md
|
|
20
|
+
- ../features/persona-gated-navigation.md
|
|
21
|
+
- ../../test/features/github-audit-script.md
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Summary
|
|
25
|
+
|
|
26
|
+
An internal report at **`/developers/github-audit`** (Developers nav group, personas
|
|
27
|
+
`['Development Team','TOGa Technology']`) that shows, for one calendar month, the **lines
|
|
28
|
+
added/removed across every repo in the `agilantsolutions` GitHub org**, plus commit count,
|
|
29
|
+
unique authors, and active repo count. Built so the team gets a monthly code-churn report
|
|
30
|
+
without leaving GitHub.
|
|
31
|
+
|
|
32
|
+
It is the app port of the standalone
|
|
33
|
+
[`test/team/github_audit.php`](../../test/features/github-audit-script.md)
|
|
34
|
+
script (built first, then moved into Tools). The engine reuses the **Design tool's GitHub
|
|
35
|
+
plumbing** — same token, same log-detail/return-generic error pattern.
|
|
36
|
+
|
|
37
|
+
## Key files / entry points
|
|
38
|
+
|
|
39
|
+
- `_/app/github/audit.php` — class **`App_Github_Audit`**: a static, read-only GitHub REST
|
|
40
|
+
engine (not a Controller/Model). Mirrors the sibling `App_Design_Github` static-engine
|
|
41
|
+
pattern.
|
|
42
|
+
- `mvc/developers/github-audit/get.php` — the page: native `<input type="month">` picker
|
|
43
|
+
(defaults to last month), a Run button, a progress bar, and results (summary cards +
|
|
44
|
+
per-repo table). Themed with `--c-*` tokens (light/dark).
|
|
45
|
+
- `mvc/developers/github-audit/post.php` — AJAX JSON endpoint. `requireAuth` +
|
|
46
|
+
`App_Page::validateCrossSiteRequestForgery()`; allowlisted actions only.
|
|
47
|
+
- `_/app/nav.php` — one nav action under the `developers` group.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
### Shared token (depends on the Design tool)
|
|
52
|
+
|
|
53
|
+
The audit engine does **not** define its own token config. It reuses
|
|
54
|
+
`App_Design_Github::token()` — the `GITHUB_TOKEN` env property first, then the
|
|
55
|
+
`[design] github_token` config fallback. So the GitHub token is **shared** between the
|
|
56
|
+
Design tool and the audit tool, which drives the concurrency lock below.
|
|
57
|
+
|
|
58
|
+
### Accuracy — per-commit exact counting
|
|
59
|
+
|
|
60
|
+
To get exact line counts, the engine lists **each commit** in the month window
|
|
61
|
+
(`since`/`until`), then fetches **each commit's stats**. Merge commits are **skipped** (they
|
|
62
|
+
would double-count), and forks are **skipped**.
|
|
63
|
+
|
|
64
|
+
### Excluded repos
|
|
65
|
+
|
|
66
|
+
`App_Github_Audit` has a top-of-class `const EXCLUDED_REPOS = ['test', 'claude'];` — a small
|
|
67
|
+
list of repo names to skip entirely. Matched **case-insensitively** in `fetchRepos()`, so
|
|
68
|
+
those repos never enter the run. Add more names here to skip them later. This is in addition
|
|
69
|
+
to the existing skip-forks / (optional) skip-archived filters.
|
|
70
|
+
|
|
71
|
+
### Two-phase run + real progress bar
|
|
72
|
+
|
|
73
|
+
The report can hit hundreds of API calls, so the run is split so the page can show true
|
|
74
|
+
progress:
|
|
75
|
+
|
|
76
|
+
1. `action=repos` — one call that lists all org repos.
|
|
77
|
+
2. `action=repo` — audits **one** repo per request (repo N of total).
|
|
78
|
+
|
|
79
|
+
The client accumulates totals and authors across the per-repo responses and renders at the
|
|
80
|
+
end. If the run stops midway, it shows **partial results**.
|
|
81
|
+
|
|
82
|
+
### Reliability — retries and a budget gate at 1000 calls
|
|
83
|
+
|
|
84
|
+
- GitHub `202` "stats not ready" → retried, then counted as `0` if still not ready.
|
|
85
|
+
- Secondary / abuse rate limit (`403` + `Retry-After`) → retried.
|
|
86
|
+
- **Budget gate** — `const MIN_REMAINING_TO_RUN = 1000;` sets the floor. Enforced at two
|
|
87
|
+
points, both throwing a user-safe (`ERR_USER_SAFE`) message:
|
|
88
|
+
- **Pre-flight:** `App_Github_Audit::assertBudget()` calls `GET /rate_limit` (which does
|
|
89
|
+
**not** count against the rate limit) at the start of a run. If the shared token's core
|
|
90
|
+
`remaining` is **at or below 1000**, it refuses to start and the scan never begins.
|
|
91
|
+
- **Mid-run:** `assertRateLimitOk()` **stops** the run once `x-ratelimit-remaining` drops
|
|
92
|
+
to 1000 — read from headers already returned, so no extra calls — and shows partial
|
|
93
|
+
results.
|
|
94
|
+
- This replaced the earlier tiny `RATE_LIMIT_FLOOR=25` safety. Rationale: the GitHub token is
|
|
95
|
+
**shared with the Design tool**, so the audit must leave a healthy budget for other work.
|
|
96
|
+
|
|
97
|
+
### Live API-call + budget counter
|
|
98
|
+
|
|
99
|
+
- `App_Github_Audit::callCount()` — calls made in this PHP request.
|
|
100
|
+
- `App_Github_Audit::rateLimit()` — GitHub `x-ratelimit-*` headers (used / remaining /
|
|
101
|
+
limit / reset).
|
|
102
|
+
|
|
103
|
+
The page shows a live line: `API calls this run: N · GitHub budget: X of 5,000 left`, and
|
|
104
|
+
the total in the results caption.
|
|
105
|
+
|
|
106
|
+
### Per-session concurrency lock (shared-token protection)
|
|
107
|
+
|
|
108
|
+
`post.php` takes an **APCu lock** (`apcu_add`, key `gha_audit_lock_<session_id>`, 300s TTL)
|
|
109
|
+
so only one audit request runs at a time per session. Reason: the GitHub token is **shared**
|
|
110
|
+
with the Design tool, so parallel scans would burn the org's rate limit for everyone. If
|
|
111
|
+
APCu is unavailable the lock is skipped cleanly.
|
|
112
|
+
|
|
113
|
+
- **Known limit:** APCu is per-EB-instance, so the lock is **per-instance**, not global.
|
|
114
|
+
|
|
115
|
+
### Error sanitizing
|
|
116
|
+
|
|
117
|
+
Only a **user-safe** `RuntimeException` (the rate-limit notice, thrown with code
|
|
118
|
+
`App_Github_Audit::ERR_USER_SAFE`) reaches the browser. Every other `RuntimeException` /
|
|
119
|
+
`Throwable` is logged and returned as a **generic** message — GitHub's raw text and repo
|
|
120
|
+
names never surface. Same log-detail / return-generic pattern as
|
|
121
|
+
`App_Design_Github::commitError()`.
|
|
122
|
+
|
|
123
|
+
### Input validation & SSRF guard
|
|
124
|
+
|
|
125
|
+
- **Month:** must match `^\d{4}-\d{2}$` **and** month number 01–12. The regex alone accepts
|
|
126
|
+
`2026-99`, which `DateTime::createFromFormat` silently overflows.
|
|
127
|
+
- **Repo name:** must match `^[A-Za-z0-9._-]{1,100}$` and is rejected if it contains `..`.
|
|
128
|
+
- `org` / `repo` / `sha` are `rawurlencode`'d.
|
|
129
|
+
- `$_POST` reads are guarded with `is_string()` before the `(string)` cast — an array value
|
|
130
|
+
would raise an **uncatchable** PHP warning → `App_Error` exit → HTML instead of JSON.
|
|
131
|
+
- **SSRF:** host is pinned to `api.github.com`; Link-header pagination is followed only when
|
|
132
|
+
the host is still `api.github.com`; curl is pinned to `CURLPROTO_HTTPS` with SSL verify on.
|
|
133
|
+
|
|
134
|
+
## Access control
|
|
135
|
+
|
|
136
|
+
Behind Tools SSO. `get.php` and `post.php` self-guard with `requireAuth` +
|
|
137
|
+
`App_Page::validateCrossSiteRequestForgery()`; the nav action is gated to the
|
|
138
|
+
`Development Team` and `TOGa Technology` personas.
|
|
139
|
+
|
|
140
|
+
## GitHub token
|
|
141
|
+
|
|
142
|
+
No new config. Uses `App_Design_Github::token()` — see
|
|
143
|
+
[Design Demo Admin › GitHub token](./design-demo-admin.md). Value never recorded, logged, or
|
|
144
|
+
echoed.
|
|
145
|
+
|
|
146
|
+
## Client variations
|
|
147
|
+
|
|
148
|
+
None — internal/shared developer tool.
|
|
149
|
+
|
|
150
|
+
## Gotchas / known issues
|
|
151
|
+
|
|
152
|
+
- **The `hidden` attribute is beaten by a class `display:` rule.** The results/spinner box
|
|
153
|
+
had the HTML `hidden` attribute but still showed on load (looked like the tool auto-ran),
|
|
154
|
+
because an author `display:flex` class rule beats the UA `[hidden]{display:none}`. Fix: a
|
|
155
|
+
scoped guard `.<scope> [hidden] { display:none !important; }` (or `:not([hidden])` on the
|
|
156
|
+
display rule). General server-rendered-UI trap.
|
|
157
|
+
- **`curl_close()` is fatal on PHP 8.5** in the 1.0 framework (deprecation → `ErrorException`
|
|
158
|
+
→ aborted request). The audit engine avoids `curl_close()` — same rule as the Design tool.
|
|
159
|
+
See [Tools architecture](../architecture.md) and framework-rules.
|
|
160
|
+
- **APCu lock is per-EB-instance**, so the single-run guard is per-instance, not org-wide.
|
|
161
|
+
- **Shared rate-limit budget** — this tool and the Design tool draw on the same GitHub token,
|
|
162
|
+
so a big audit eats into the Design tool's budget and vice versa.
|
|
163
|
+
|
|
164
|
+
## Change history
|
|
165
|
+
|
|
166
|
+
- 2026-09-04 — Added `EXCLUDED_REPOS = ['test','claude']` (case-insensitive skip in
|
|
167
|
+
`fetchRepos()`). Replaced `RATE_LIMIT_FLOOR=25` with a 1000-call budget gate
|
|
168
|
+
(`MIN_REMAINING_TO_RUN`): pre-flight `assertBudget()` via free `GET /rate_limit`, plus
|
|
169
|
+
mid-run `assertRateLimitOk()` stop — both user-safe — to leave budget for the shared Design
|
|
170
|
+
tool token. (jcardinal)
|
|
171
|
+
- 2026-09-04 — Created. New `/developers/github-audit` report over the `agilantsolutions` org
|
|
172
|
+
(lines +/-, commits, unique authors, active repos, per month). Per-commit exact counting
|
|
173
|
+
(merges/forks skipped), 202 + secondary-rate-limit retries with a primary-limit floor,
|
|
174
|
+
two-phase repos→repo progress, live API-call/budget counter, per-session APCu concurrency
|
|
175
|
+
lock (shared Design-tool token), user-safe-only error surfacing, and month/repo input +
|
|
176
|
+
SSRF hardening. Ported from `test/team/github_audit.php`. (jcardinal)
|
|
177
|
+
|
|
178
|
+
## Related docs
|
|
179
|
+
|
|
180
|
+
- [Tools Architecture](../architecture.md)
|
|
181
|
+
- [Design Demo Admin](./design-demo-admin.md) — source of the shared GitHub token and error pattern
|
|
182
|
+
- [Front-End Standards](../../../standards/frontend.md)
|
|
183
|
+
- [github_audit.php standalone script](../../test/features/github-audit-script.md)
|
|
184
|
+
</content>
|
|
185
|
+
</invoke>
|