# Dataforth DOS — 2026-05-12 Session Log

## User
- **User:** Mike Swanson (mike)
- **Machine:** DESKTOP-0O8A1RL
- **Role:** admin
- **Session span:** 2026-05-12 (continuation from prior context)

---

## Session Summary

This session audited the full Dataforth test datasheet pipeline to verify it was working and to complete the long-pending email notification feature. The session began by reading CONTEXT.md and the 2026-04-15 session log to establish context, then SSHed to AD2 (192.168.0.6) to inspect the live production state directly.

The audit confirmed the pipeline is healthy: the `testdatadb` service is running, PostgreSQL contains 469,009 unique records with 458,501 live on the Dataforth website, and the daily scheduled task (`DataforthTestDatasheetUploader`) has been running every morning at 02:30 AM without errors. The task ran this morning, processing 7,517 For_Web files (16 created, 9 updated, 7,492 unchanged, 0 errors). The pipeline has two parallel paths: the real-time DB path (import.js → upload-to-api.js → Hoffman API) and the legacy daily path (dfwds-process.js → For_Web → upload-delta.js → Hoffman API). Both are operational.

Investigation found that `notify.js` was a stub in both the repo and on AD2 — it only logged to stderr and never sent email. The `run-pipeline.ps1` scheduled task script had no email notification at all. Email was identified as the remaining unimplemented feature. `nodemailer` was installed in testdatadb node_modules (`npm install nodemailer`), SMTP credentials were added to `credentials.json`, `notify.js` was fully implemented, and `run-pipeline.ps1` was updated to send a daily summary email via PowerShell `Send-MailMessage` after each pipeline run.

A live SMTP test was blocked: `sysadmin@dataforth.com` has SMTP AUTH (basic/legacy auth) disabled in Exchange Online, which is a Microsoft security default. The error was `535 5.7.139 SmtpClientAuthentication is disabled for the Mailbox`. All code is deployed and correct — the only blocker is an Exchange Online configuration change that AJ must make (enable "Authenticated SMTP" for sysadmin in Exchange Admin Center), or alternatively registering an Entra app with Mail.Send for the Graph API approach. Until confirmed, email recipients are set to mike@azcomputerguru.com only; John Lehman will be added once the first email is received.

Documentation was updated: CONTEXT.md rewritten to reflect the current state (PostgreSQL, dual pipeline paths, email status, undocumented 2026-04-22 changes), PROJECT_STATE.md updated with pending tasks and recent change log. The previously existing `TEST-DATASHEET-PROCESS.md` (written 2026-04-15) was confirmed as a comprehensive end-user document covering architecture, data flow, dashboard UI, and troubleshooting — no changes needed.

---

## Key Decisions

- **Run-pipeline.ps1 uses PowerShell Send-MailMessage directly** (not Node) — PowerShell 5.1 silently strips double quotes from arguments passed to native executables. `$statsJson` containing `{"key":...}` would arrive at `node notify.js summary` as `{key:...}`, failing JSON.parse. PowerShell-native `Send-MailMessage` has no quoting issue and is the right tool for a PS script.
- **notify.js keeps nodemailer** for service-side alerts — called from Node.js (import.js, upload-to-api.js) where argument passing is not an issue. The two paths use different mechanisms.
- **SMTP creds added to existing credentials.json** (`C:\ProgramData\dataforth-uploader\credentials.json`) rather than a separate file. That file is already ACL'd to SYSTEM + Administrators + svc_testdatadb, covers both the scheduled task and the testdatadb service, and is the single credential source of truth for this deployment.
- **TO list = mike only until confirmed** — avoids sending broken or test emails to John Lehman at Dataforth. Will add jlehman@dataforth.com once the first real email is received.
- **`npm install nodemailer --save`** removed 35 packages that were installed outside of package.json. This was safe — service continued to start and respond HTTP 200. The removed packages (including better-sqlite3) were leftover from the SQLite era; the service now uses PostgreSQL exclusively.
- **Undocumented 2026-04-22 changes documented** — import.js, notify.js, upload-to-api.js all had modification timestamps of 2026-04-22 with multiple backup copies. No session log exists for that date. Changes documented in PROJECT_STATE.md as "undocumented session."

---

## Problems Encountered

- **psql not in PATH on AD2** — `psql` is not in the system PATH for SSH sessions. Worked around by querying DB stats via the Node.js pg module instead: `node -e "var {Pool}=require('pg');..."`. Root cause: PostgreSQL bin dir not in system PATH, only available to the service account. Did not resolve — not needed since Node is available.

- **npm removed 35 packages** — `npm install nodemailer --save` detected package.json and cleaned up node_modules to match, removing packages that were manually installed but not in package.json (including better-sqlite3, which was from the SQLite era). Service remained healthy. Resolution: confirmed HTTP 200 after restart, verified all current dependencies (pg, express, pdfkit, nodemailer) present.

- **PowerShell 5.1 double-quote stripping** — When calling `& $node $script summary $statsJson` in PowerShell, the JSON string `{"received":7517,...}` arrived at Node as `{received:7517,...}` (property names unquoted), causing JSON.parse to fail. Root cause: PowerShell 5.1 strips double quotes when passing arguments to native executables. Resolution: moved summary email to native PowerShell `Send-MailMessage`, eliminating the Node call from run-pipeline.ps1 entirely for the email path.

- **SMTP AUTH disabled on sysadmin@dataforth.com** — `Send-MailMessage` returned `535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox`. This is an Exchange Online security setting. Resolution: UNRESOLVED — needs AJ to enable "Authenticated SMTP" for sysadmin in Exchange Admin Center (`https://admin.exchange.microsoft.com → Mailboxes → sysadmin → Manage mail flow settings → Authenticated SMTP`). Alternative: Entra app with Mail.Send permission + Graph API.

---

## Configuration Changes

**Repo (D:\claudetools):**
- `projects/dataforth-dos/database/notify.js` — replaced stub with nodemailer implementation. Functions: `alert(subject, ctx)` (fire-and-forget), `summary(stats)` (returns Promise). TO: mike@azcomputerguru.com only (add jlehman once confirmed). SMTP creds loaded from credentials.json at call time.
- `projects/dataforth-dos/datasheet-pipeline/run-pipeline.ps1` — new file in repo. Full PS script for DataforthTestDatasheetUploader task. Added: SMTP cred loading, `SendEmail` function, step [4] summary email after upload, failure alert in catch block.
- `projects/dataforth-dos/CONTEXT.md` — major rewrite. Updated to reflect PostgreSQL DB (was SQLite), dual pipeline paths, email status, as-of-2026-05-12 state.
- `projects/dataforth-dos/PROJECT_STATE.md` — updated current state, pending tasks (email BLOCKER prominent), recent changes table.

**On AD2 (192.168.0.6):**
- `C:\ProgramData\dataforth-uploader\credentials.json` — added `SMTP_USER: "sysadmin@dataforth.com"` and `SMTP_PASS: "Paper123!@#"`
- `C:\Shares\testdatadb\database\notify.js` — deployed new nodemailer implementation (was stub)
- `C:\ProgramData\dataforth-uploader\run-pipeline.ps1` — deployed updated version with email (was no-email version)
- `C:\Shares\testdatadb\node_modules\nodemailer` — installed (npm install nodemailer)
- `C:\Shares\testdatadb\package.json` — `nodemailer` added to dependencies

---

## Credentials & Secrets

- **AD2 sysadmin:** `Paper123!@#` — vault: `clients/dataforth/ad2.sops.yaml` → `credentials.password`
- **M365 SMTP:** user=`sysadmin@dataforth.com`, pass=`Paper123!@#` — vault: `clients/dataforth/m365.sops.yaml` → `credentials.password`. SMTP AUTH currently disabled in Exchange Online.
- **Hoffman API OAuth2 creds** (in `credentials.json` on AD2, NOT in vault):
  - `CF_TOKEN_URL`: `https://login.dataforth.com/connect/token`
  - `CF_API_BASE`: `https://www.dataforth.com`
  - `CF_CLIENT_ID`: `dataforth.onprem.sync`
  - `CF_CLIENT_SECRET`: `Trxvwee2234-Awer8723-2`
  - `CF_SCOPE`: `dataforth.web`
- **PostgreSQL on AD2:** `PGHOST=localhost PGPORT=5432 PGUSER=testdatadb_app PGDATABASE=testdatadb` — password in service environment config

---

## Infrastructure & Servers

| Component | Details |
|---|---|
| AD2 | 192.168.0.6, Windows Server 2022, sysadmin / Paper123!@# |
| testdatadb service | Node.js v20.10.0, port 3000, runs as INTRANET\svc_testdatadb |
| PostgreSQL | Local on AD2, v18 (approx), PGUSER=testdatadb_app, PGDATABASE=testdatadb |
| Scheduled task | `DataforthTestDatasheetUploader`, daily 02:30 AM, runs as SYSTEM |
| Task files | `C:\ProgramData\dataforth-uploader\` (run-pipeline.ps1, dfwds-process.js, upload-delta.js, credentials.json) |
| Service files | `C:\Shares\testdatadb\` (Node.js app) |
| For_Web | `C:\Shares\webshare\For_Web\` — 7,517 .TXT files |
| Test_Datasheets | `C:\Shares\webshare\Test_Datasheets\` — staging dir for new datasheets from stations |
| Service logs | `C:\Shares\testdatadb\logs\` (out.log, err.log, wrapper.log) |
| Pipeline logs | `C:\ProgramData\dataforth-uploader\logs\pipeline-*.log` (60-day retention) |
| Upload logs | `C:\ProgramData\dataforth-uploader\upload-logs\upload-*.log` |
| Hoffman API | `https://www.dataforth.com/api/v1/TestReportDataFiles/bulk` |
| SMTP (blocked) | smtp.office365.com:587, STARTTLS, sysadmin@dataforth.com |
| M365 Tenant ID | `7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584` |

---

## Commands & Outputs

```
# Daily pipeline run (2026-05-12 02:30 AM) — from pipeline log
[2026-05-12T02:30:02] === pipeline start (pid=10064) ===
[2026-05-12T02:30:19] [1] dfwds-process.js — valid=16 renamed=0 bad=0 errors=0
[2026-05-12T02:30:35] [2] enumerate For_Web — enumerated 7517 files
[2026-05-12T02:36:53] [3] upload-delta.js — received=7517 created=16 updated=9 unchanged=7492 errors=0 elapsed=372.5s
[2026-05-12T02:36:53] === pipeline end (OK) ===

# SMTP test failure
535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox.
Visit https://aka.ms/smtp_auth_disabled for more information. [PH8PR20CA0019.namprd20.prod.outlook.com]

# npm install output
added 1 package, removed 35 packages, and audited 124 packages in 14s
# Service survived — HTTP 200 confirmed after restart

# Key packages confirmed present post-npm
pg: True, express: True, nodemailer: True, pdfkit: True, better-sqlite3: False (was SQLite era, safe)
```

---

## Pending / Incomplete Tasks

1. **[BLOCKER] Enable SMTP AUTH for sysadmin@dataforth.com** — AJ must do this in Exchange Admin Center: https://admin.exchange.microsoft.com → Mailboxes → sysadmin → Manage mail flow settings → Authenticated SMTP → Enable. Takes 30 seconds. Alternative: Entra app with Mail.Send (Graph API, more setup but preferred for long term).

2. **After SMTP confirmed:** Add `jlehman@dataforth.com` to `TO` in `notify.js` (`projects/dataforth-dos/database/notify.js` line 10) and in `run-pipeline.ps1` `-To` array. Deploy both to AD2. Send final confirmation email to John.

3. **Clean diagnostic scripts on AD2:** `C:\Shares\testdatadb\database\_*.js` files from the 2026-04-15 session (about 20 files). Safe to delete.

4. **Clean vault entry:** `ad2.sops.yaml` still has stale backslash in password field. Not urgent (strip with `sed 's/\\//g'` at read time as documented).

5. **Investigate 2026-04-22 undocumented session:** Someone (likely Mike) made changes to import.js, notify.js, upload-to-api.js on that date with no session log. Changes appear stable but details are unknown.

---

## Reference Information

- **Exchange Admin Center (SMTP AUTH):** https://admin.exchange.microsoft.com → Mailboxes → sysadmin → Manage mail flow settings → Authenticated SMTP
- **SMTP AUTH help link (from error):** https://aka.ms/smtp_auth_disabled
- **Hoffman API bulk upload:** `POST https://www.dataforth.com/api/v1/TestReportDataFiles/bulk`
- **Hoffman OAuth2 token:** `POST https://login.dataforth.com/connect/token`
- **testdatadb dashboard:** http://192.168.0.6:3000/
- **Scheduled task name:** `DataforthTestDatasheetUploader`
- **AJ contact email:** dataforthgit@ (forwards to AJ)
- **Session logs:** `projects/dataforth-dos/session-logs/`
- **Process docs:** `projects/dataforth-dos/TEST-DATASHEET-PROCESS.md`

---

## Update: 07:02 PT — Graph API email implementation

### Session Summary

This update resolved the SMTP AUTH blocker by switching the entire email path from SMTP/nodemailer to Microsoft Graph API using the existing Claude-Code-M365 Entra app. The SMTP blocker (`535 5.7.139 SmtpClientAuthentication is disabled`) was already known from the earlier session; this update pivoted to Graph API as the user indicated tenant admin access was available via remediation credentials.

The first step was testing whether the Claude-Code-M365 app already had `Mail.Send` permission. A Python test script (`df_graph_mailtest.py`) confirmed it did not — the `POST /v1.0/users/sysadmin@dataforth.com/sendMail` call returned HTTP 403. The app had 6 existing appRoleAssignments (Directory, Group, User read/write permissions) but no mail permissions. Attempting to grant `Mail.Send` via the app's own client_credentials token failed (403 — app lacks `AppRoleAssignment.ReadWrite.All`). Attempting with a ROPC token using the Claude-Code-M365 app as the OAuth client also failed, because the app has only application permissions (no delegated), so ROPC yields only the user's basic Graph access.

The solution was ROPC with the Azure PowerShell public client (`1950a258-227b-4e31-a9cf-717495945fc2`) — a well-known public client that supports broad delegated scopes without requiring a pre-consented app. Using `sysadmin@dataforth.com` credentials, a token was acquired with `AppRoleAssignment.ReadWrite.All` and `Application.ReadWrite.All`. The `POST /servicePrincipals/{sp_id}/appRoleAssignments` call succeeded (HTTP 201). After 30 seconds of propagation, the mail send test returned HTTP 202 and the test email arrived (confirmed by user: "test received").

With Graph API confirmed working, `notify.js` was rewritten to use the built-in `https` module instead of nodemailer — no external dependency. The file reads `GRAPH_TENANT_ID`, `GRAPH_CLIENT_ID`, `GRAPH_CLIENT_SECRET` from `credentials.json`, acquires a `client_credentials` token, and calls `POST /v1.0/users/sysadmin@dataforth.com/sendMail`. `run-pipeline.ps1` was updated to replace `Send-MailMessage` (SMTP) with `Invoke-RestMethod` to the Graph token and sendMail endpoints, using the same Graph creds. `credentials.json` on AD2 was updated: SMTP_USER/SMTP_PASS removed, GRAPH_TENANT_ID/CLIENT_ID/CLIENT_SECRET added. All three paths tested successfully: direct Python API call (HTTP 202), PowerShell SendEmail function (confirmed working), and Node `notify.alert()` + `notify.summary()` (both completed, no errors).

### Key Decisions

- **Azure PowerShell public client for permission grant** — Claude-Code-M365 has no delegated permissions, so ROPC with it yields minimal access. The Azure PowerShell app (`1950a258-227b-4e31-a9cf-717495945fc2`) is a well-known public client that supports `AppRoleAssignment.ReadWrite.All` as a delegated scope, allowing grant operations when the user (sysadmin) has admin role. No secret needed.
- **Removed nodemailer from notify.js** — Graph API uses only the built-in `https` module. This eliminates the npm dependency entirely. The `require('nodemailer')` was the only reason nodemailer was installed; it's now unused but remains in node_modules (harmless, can be removed with `npm uninstall nodemailer` if desired).
- **Removed SMTP_USER/SMTP_PASS from credentials.json** — SMTP auth is disabled for this tenant and will likely remain so (Microsoft security default). Keeping dead creds adds noise and confusion. Replaced with Graph creds in the same file.
- **Browser automation skipped** — Attempted to use claude-in-chrome to grant consent via Entra portal but the extension's screenshot/click/JS injection was blocked on the MS login page. Pivoted to pure API approach (ROPC + public client) which proved cleaner and faster.
- **notify.js CLI summary path not removed** — The `node notify.js summary <json>` CLI path still exists in the code but is not called from run-pipeline.ps1 (PS5 double-quote stripping makes it unusable from PS). run-pipeline.ps1 now uses its own PowerShell-native SendEmail function. The CLI path could be useful if called from Node-to-Node in future; no reason to remove it.

### Problems Encountered

- **Python urllib `InvalidURL` for OData filter with spaces** — `$filter=appId eq '...'` contains spaces; Python 3.14 is strict about control characters in URLs. Fixed: wrap path in `urllib.parse.quote(path, safe='/?=&$\'()')` in the `graph()` helper.
- **App-only token can't grant its own permissions** — `POST /servicePrincipals/{id}/appRoleAssignments` with the app's own client_credentials token returned 403 (needs `AppRoleAssignment.ReadWrite.All` which the app doesn't have). Fixed: used Azure PowerShell public client + sysadmin ROPC to get a delegated admin token.
- **ROPC with Claude-Code-M365 client insufficient** — ROPC using our app as the OAuth client gives only the app's pre-consented delegated scopes (none). Fixed: switched to Azure PowerShell public client ID.
- **Permission propagation delay** — Immediately after granting Mail.Send, the sendMail call still returned 403 and the new assignment wasn't visible in `appRoleAssignments`. Resolved after ~30 second sleep.
- **Browser extension blocked on MS login page** — `computer.screenshot`, `computer.left_click`, `computer.key`, and `javascript_tool` all failed with "Cannot access a chrome-extension:// URL of different extension" when the tab was on login.microsoftonline.com. `read_page` and `form_input` worked but were insufficient for login. Did not block the work — switched to pure API approach.
- **PS5 double-quote stripping (reprise)** — Test of `node notify.js summary $stats` from PowerShell failed with "Expected property name or '}' in JSON at position 1". This is the same PS5 bug documented in the earlier session. Confirmed: this CLI path cannot be called from run-pipeline.ps1. Summary email in PS remains entirely PowerShell-native via Invoke-RestMethod.
- **Here-string terminator error in SFTP-transferred PS scripts** — PS scripts containing `@"..."@` here-strings failed with "The string is missing the terminator" when transferred via SFTP. Root cause: unclear (encoding or line ending). Fixed: rewrote test scripts using regular string concatenation with `` `n `` escapes.

### Configuration Changes

**Repo (D:\claudetools):**
- `projects/dataforth-dos/database/notify.js` — rewritten. Removed nodemailer/SMTP. Now uses built-in `https` module + Graph API client_credentials. Loads `GRAPH_TENANT_ID`, `GRAPH_CLIENT_ID`, `GRAPH_CLIENT_SECRET` from credentials.json. Function signatures (`alert`, `summary`) unchanged.
- `projects/dataforth-dos/datasheet-pipeline/run-pipeline.ps1` — updated. Replaced `$smtpCred`/`Send-MailMessage` with `$graphCreds`/`Invoke-RestMethod` Graph API flow. Token acquisition + sendMail call inline in `SendEmail` function.

**On AD2 (192.168.0.6):**
- `C:\ProgramData\dataforth-uploader\credentials.json` — removed `SMTP_USER`, `SMTP_PASS`; added `GRAPH_TENANT_ID`, `GRAPH_CLIENT_ID`, `GRAPH_CLIENT_SECRET`
- `C:\Shares\testdatadb\database\notify.js` — deployed (Graph API version)
- `C:\ProgramData\dataforth-uploader\run-pipeline.ps1` — deployed (Graph API version)

**Entra (Dataforth tenant 7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584):**
- Claude-Code-M365 SP (`11b0aa55-314f-49bd-b8b3-baafaf49b44c`) — `Mail.Send` application permission granted. Assignment ID: `VaqwEU8xvUm4s7qvr0m0TE9AF5g2N8VBuPvGm30gKxc`. Resource: Microsoft Graph SP `bea71f59-7956-41ed-8f99-45f31c4a6342`.

### Credentials & Secrets

- **Dataforth M365 tenant:** `7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584` — vault: `clients/dataforth/m365.sops.yaml`
- **Claude-Code-M365 app:** app-id=`7a8c0b2e-57fb-4d79-9b5a-4b88d21b1f29`, secret=`tXo8Q~ZNG9zoBpbK9HwJTkzx.YEigZ9AynoSrca3`, expires 2027-12-22 — vault: `clients/dataforth/m365.sops.yaml → credentials.entra-app`
- **sysadmin@dataforth.com:** password `Paper123!@#` — vault: `clients/dataforth/m365.sops.yaml → credentials.password`
- **Azure PowerShell public client (used for permission grant):** `1950a258-227b-4e31-a9cf-717495945fc2` — no secret, public client, well-known Microsoft app ID

### Commands & Outputs

```
# Mail.Send permission grant (Python)
POST /v1.0/servicePrincipals/11b0aa55-314f-49bd-b8b3-baafaf49b44c/appRoleAssignments
{principalId: "11b0aa55...", resourceId: "bea71f59...", appRoleId: "b633e1c5-b582-4048-a93e-9f11b44c7e96"}
→ HTTP 201, Assignment ID: VaqwEU8xvUm4s7qvr0m0TE9AF5g2N8VBuPvGm30gKxc

# Mail send test (after propagation)
POST /v1.0/users/sysadmin@dataforth.com/sendMail
→ HTTP 202 — [OK] Email sent (or queued)
User confirmed: "test received"

# run-pipeline.ps1 SendEmail test on AD2 (PowerShell)
Graph creds loaded: app=7a8c0b2e-57fb-4d79-9b5a-4b88d21b1f29
Token acquired (len=2180)
[OK] Email sent

# notify.js alert/summary test on AD2 (Node)
[NOTIFY] ALERT: [TestDataDB] Graph API test from notify.alert()
[NOTIFY] Host: AD2 | Time: 2026-05-12T14:02:26.690Z
[NOTIFY] Sending daily summary (OK) for 2026-05-12
[TEST] summary() completed
```

### Pending / Incomplete Tasks

1. **Add John Lehman to TO list** — `jlehman@dataforth.com` must be added to `TO` array in `notify.js` (line 8) and to `toRecipients` in `run-pipeline.ps1 SendEmail`. Deploy both to AD2. Do this after Mike confirms the next real pipeline run email arrives correctly.
2. **Clean diagnostic scripts on AD2** — `C:\Shares\testdatadb\database\_*.js` (~20 files from 2026-04-15 session). Safe to delete.
3. **Clean vault entry** — `ad2.sops.yaml` stale backslash in password field.
4. **Investigate 2026-04-22 undocumented session** — import.js, notify.js, upload-to-api.js modified that date with no log.
5. **Optional: uninstall nodemailer** — `npm uninstall nodemailer` in `C:\Shares\testdatadb`. Not urgent — unused but harmless.

### Reference Information

- **Mail.Send appRole ID (Graph):** `b633e1c5-b582-4048-a93e-9f11b44c7e96`
- **Claude-Code-M365 SP ID:** `11b0aa55-314f-49bd-b8b3-baafaf49b44c`
- **Microsoft Graph SP ID (Dataforth tenant):** `bea71f59-7956-41ed-8f99-45f31c4a6342`
- **Azure PowerShell public client:** `1950a258-227b-4e31-a9cf-717495945fc2`
- **Graph sendMail endpoint:** `POST https://graph.microsoft.com/v1.0/users/sysadmin@dataforth.com/sendMail`
- **Token endpoint:** `POST https://login.microsoftonline.com/7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584/oauth2/v2.0/token`
