Add Dataforth process docs + Azure signing attestation letter
- TEST-DATASHEET-PROCESS.md: comprehensive pipeline documentation for Dataforth engineering (10 sections, data flow, state diagram, FAQ) - signing-attestation/: domain ownership attestation letter with in-place signature for Azure Trusted Signing identity validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
473
projects/dataforth-dos/TEST-DATASHEET-PROCESS.md
Normal file
473
projects/dataforth-dos/TEST-DATASHEET-PROCESS.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# Test Datasheet Pipeline — Process Documentation
|
||||
|
||||
**Audience:** Dataforth Engineering
|
||||
**Last Updated:** 2026-04-15
|
||||
**System Owner:** AZ Computer Guru (Mike Swanson)
|
||||
**Environment:** Dataforth Tucson production
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
The **Test Datasheet Pipeline** captures every passing unit tested on Dataforth's test stations, converts the raw test log into a formatted datasheet document, pushes that document to Dataforth's public-facing product website, and gives internal staff a dashboard to search, review, and manually push records as needed.
|
||||
|
||||
As of 2026-04-15 the system contains **469,009 unique test records** covering **192 SCM7B models, 238 SCM5B models, 129 8B models, 214 DSCA models, and 37 DSCT models**. Of those:
|
||||
|
||||
- **458,501 are live on the Dataforth website** (Hoffman Product API)
|
||||
- **10,508 are retained internally** but not on the website, broken down as:
|
||||
- 7,905 — unit tested PASS but no model specs available locally to render the datasheet (waiting on spec data)
|
||||
- 2,426 — Hoffman API rejected (data quality issue to investigate per record)
|
||||
- 177 — unit failed the test (by engineering policy, FAIL records never appear on the website)
|
||||
|
||||
The website total (661,367 records) exceeds our internal database because the website also holds historical units from **before the current testdatadb system existed**. That legacy data was uploaded by prior tools (DFWDS) and is not reproducible from our current DB.
|
||||
|
||||
---
|
||||
|
||||
## 2. System Architecture
|
||||
|
||||
### 2.1 Component Overview
|
||||
|
||||
```
|
||||
┌───────────────────────┐ ┌────────────────────────┐
|
||||
│ Test Stations │ │ Legacy DFWDS (VB6) │
|
||||
│ TS-01 ... TS-27 │ │ (watched .dat files, │
|
||||
│ produce .dat log │ │ produced For_Web │
|
||||
│ files per test run │ │ .TXT files) │
|
||||
└──────────┬────────────┘ └──────────┬─────────────┘
|
||||
│ │
|
||||
│ writes to │ (historical, superseded
|
||||
▼ │ by this pipeline)
|
||||
┌──────────────────────────────────────────▼─────────────┐
|
||||
│ AD1 HISTLOGS Share │
|
||||
│ C:\Shares\test\Ate\HISTLOGS\{log_type}\{model}.DAT │
|
||||
│ Per-station mirrors: \TS-XX\LOGS\{log_type}\ │
|
||||
└──────────┬─────────────────────────────────────────────┘
|
||||
│ scanned by
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ testdatadb service (AD2, 192.168.0.6:3000) │
|
||||
│ - Node.js + Express API │
|
||||
│ - PostgreSQL 18 backend │
|
||||
│ - Web dashboard at http://192.168.0.6:3000/ │
|
||||
│ - WinSW service wrapper │
|
||||
│ - Service account: INTRANET\svc_testdatadb │
|
||||
└──────────┬─────────────────────────────────────────────┘
|
||||
│ HTTPS POST
|
||||
▼
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ Dataforth Hoffman Product API │
|
||||
│ /api/v1/TestReportDataFiles/bulk │
|
||||
│ OAuth2 client_credentials │
|
||||
│ Serves datasheets on product pages (public website) │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Server inventory
|
||||
|
||||
| Component | Host | Purpose |
|
||||
|---|---|---|
|
||||
| Test station logs | AD1 (SMB `\\ad1\...`) | Central HISTLOGS store + per-station mirrors |
|
||||
| Application + DB | AD2 (192.168.0.6) | testdatadb Node.js service + PostgreSQL 18 |
|
||||
| For_Web legacy output | AD2 (`C:\Shares\webshare\For_Web`) | Historical intermediate; being phased out |
|
||||
| Credentials | AD2 (`C:\ProgramData\dataforth-uploader\credentials.json`) | OAuth creds for Hoffman API, ACL'd to SYSTEM + Administrators + svc_testdatadb |
|
||||
| Schedule fallback | AD2 (Task Scheduler: daily 02:30) | Run-as-SYSTEM safety net if real-time upload fails |
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Flow — Step by Step
|
||||
|
||||
### 3.1 Step 1: Test station produces a log file
|
||||
|
||||
A test station (e.g., TS-27) completes a unit test and appends a record to its local `.dat` file *and* to the central HISTLOGS `.dat`. File layout:
|
||||
|
||||
```
|
||||
C:\Shares\test\Ate\HISTLOGS\5BLOG\48-01.dat ← central, all TS-XX combined
|
||||
C:\Shares\test\TS-27\LOGS\5BLOG\48-01.DAT ← per-station
|
||||
```
|
||||
|
||||
The `.dat` files are **QuickBASIC random-access binary files** containing fixed-width structured records. Each record includes:
|
||||
|
||||
- Serial number (e.g., `172789-7`)
|
||||
- Model number (e.g., `SCM5B48-01`)
|
||||
- Test date + time
|
||||
- Measured values (per parameter, e.g. accuracy %, linearity, supply current)
|
||||
- Overall result (PASS / FAIL)
|
||||
- Raw readings
|
||||
|
||||
Log type naming convention:
|
||||
|
||||
| Log type | Product family | Spec file |
|
||||
|---|---|---|
|
||||
| 5BLOG | SCM5B (isolated signal conditioning) | 5BMAIN.DAT, 5B45DATA.DAT, 5B49_2.DAT, DB5B48.DAT |
|
||||
| 7BLOG | SCM7B | 7BMAIN.DAT |
|
||||
| 8BLOG | 8B | 8BMAIN.DAT |
|
||||
| DSCLOG | DSCA | DSCMAIN4.DAT, DSCOUT.DAT |
|
||||
| SCTLOG | DSCT | SCTMAIN.DAT |
|
||||
| PWRLOG | Power supplies | (specs embedded in parser) |
|
||||
| VASLOG | SCMVAS (voltage/amplitude sensing) | (specs embedded) |
|
||||
| VASLOG_ENG | SCMVAS (customer engineering variants) | (verbatim files, no template) |
|
||||
| SHT | Short-form | (specs embedded) |
|
||||
|
||||
### 3.2 Step 2: Import to database
|
||||
|
||||
The testdatadb service (`C:\Shares\testdatadb\database\import.js`) scans the HISTLOGS directories, parses the `.dat` binary files, and inserts each record into the PostgreSQL `test_records` table.
|
||||
|
||||
**Key schema:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE test_records (
|
||||
id SERIAL PRIMARY KEY,
|
||||
log_type VARCHAR(20) NOT NULL,
|
||||
model_number VARCHAR(100) NOT NULL,
|
||||
serial_number VARCHAR(100) NOT NULL,
|
||||
test_date DATE,
|
||||
test_station VARCHAR(50),
|
||||
overall_result VARCHAR(10), -- 'PASS' or 'FAIL'
|
||||
raw_data TEXT, -- decoded record text
|
||||
source_file TEXT, -- original .dat path
|
||||
work_order VARCHAR(50),
|
||||
datasheet_exported_at TIMESTAMPTZ,
|
||||
forweb_exported_at TIMESTAMPTZ, -- legacy: when For_Web .TXT was written
|
||||
api_uploaded_at TIMESTAMPTZ, -- when successfully pushed to Hoffman
|
||||
import_date TIMESTAMPTZ DEFAULT NOW(),
|
||||
search_vector tsvector,
|
||||
CONSTRAINT uq_test_records_sn UNIQUE (serial_number)
|
||||
);
|
||||
```
|
||||
|
||||
**Uniqueness rule:** one row per serial number. If a unit is re-tested, the row is **updated**, not duplicated.
|
||||
|
||||
### 3.3 Step 3: Conflict resolution — FAIL → PASS retest logic
|
||||
|
||||
Engineering directive: a unit that initially fails, gets repaired, and retests successfully should appear on the website with the **passing** datasheet. A unit that already passed and later shows up as failing should **not** be downgraded (retesting after shipping is treated as informational).
|
||||
|
||||
The import `INSERT ... ON CONFLICT` logic encodes this precisely:
|
||||
|
||||
```sql
|
||||
INSERT INTO test_records (log_type, model_number, serial_number, test_date,
|
||||
test_station, overall_result, raw_data, source_file)
|
||||
VALUES (...)
|
||||
ON CONFLICT (serial_number) DO UPDATE SET
|
||||
log_type = EXCLUDED.log_type,
|
||||
model_number = EXCLUDED.model_number,
|
||||
test_date = EXCLUDED.test_date,
|
||||
test_station = EXCLUDED.test_station,
|
||||
overall_result = EXCLUDED.overall_result,
|
||||
raw_data = EXCLUDED.raw_data,
|
||||
source_file = EXCLUDED.source_file,
|
||||
api_uploaded_at = NULL, -- force re-push on next upload run
|
||||
forweb_exported_at = NULL
|
||||
WHERE test_records.overall_result = 'FAIL'
|
||||
OR (EXCLUDED.overall_result = 'PASS'
|
||||
AND EXCLUDED.test_date > test_records.test_date)
|
||||
```
|
||||
|
||||
**Behavior table:**
|
||||
|
||||
| Existing row | New record | Result |
|
||||
|---|---|---|
|
||||
| FAIL (any date) | PASS (any date) | Row updates to PASS; website push re-queued |
|
||||
| FAIL 2026-01-01 | FAIL 2026-02-01 | Row updates to newer FAIL data |
|
||||
| PASS 2026-01-01 | PASS 2026-02-01 | Row updates to newer PASS; website push re-queued |
|
||||
| PASS 2026-02-01 | PASS 2026-01-01 | Ignored (stale import) |
|
||||
| PASS 2026-02-01 | FAIL 2026-03-01 | Ignored (PASS stays; FAIL not shown) |
|
||||
|
||||
Verified with 5 scenario tests 2026-04-15.
|
||||
|
||||
**Important:** because `api_uploaded_at` is cleared on any row update, the next upload run automatically re-pushes the record so the website gets the fresh data.
|
||||
|
||||
### 3.4 Step 4: Render the datasheet (in-memory)
|
||||
|
||||
When a record needs to be pushed to the website, the testdatadb service renders the formatted datasheet text **in memory** from the DB row.
|
||||
|
||||
Code: `C:\Shares\testdatadb\database\render-datasheet.js` (~30 lines)
|
||||
|
||||
```javascript
|
||||
const { loadAllSpecs, getSpecs } = require('../parsers/spec-reader');
|
||||
const { generateExactDatasheet } = require('../templates/datasheet-exact');
|
||||
|
||||
function renderContent(record) {
|
||||
if (record.log_type === 'VASLOG_ENG') {
|
||||
return record.raw_data || null; // verbatim customer file
|
||||
}
|
||||
const specs = getSpecs(loadAllSpecs(), record.model_number);
|
||||
if (!specs) return null; // missing specs → skip
|
||||
return generateExactDatasheet(record, specs) || null;
|
||||
}
|
||||
```
|
||||
|
||||
The generated text matches the legacy QuickBASIC DATASHEETWRITE output byte-for-byte. Example output header for `172789-7` (SCM5B48-01):
|
||||
|
||||
```
|
||||
DATAFORTH CORPORATION Phone: (520) 741-1404
|
||||
3331 E. Hemisphere Loop Fax: (520) 741-0762
|
||||
Tucson, AZ 85706 USA email: info@dataforth.com
|
||||
|
||||
TEST DATA SHEET
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Date: 12-02-2024
|
||||
Model: SCM5B48-01
|
||||
SN: 172789-7
|
||||
|
||||
ACCURACY TEST
|
||||
Calculated Measured
|
||||
Vin (mV) Vout (V) Vout (V)* Error (%) Status
|
||||
======= ========== ========== ========= ========
|
||||
...
|
||||
```
|
||||
|
||||
**Historical note:** Previously, the service wrote these files to `C:\Shares\webshare\For_Web\{SN}.TXT` via `export-datasheets.js`. That filesystem intermediate has been retired — datasheets are now rendered directly from the DB at upload time. The For_Web directory still exists for backward compatibility with legacy consumers but the testdatadb upload path no longer depends on it.
|
||||
|
||||
### 3.5 Step 5: Upload to Hoffman API
|
||||
|
||||
Code: `C:\Shares\testdatadb\database\upload-to-api.js`
|
||||
|
||||
**Endpoint:** `POST {CF_API_BASE}/api/v1/TestReportDataFiles/bulk`
|
||||
|
||||
**Authentication:** OAuth2 client_credentials flow. Token cached for its advertised lifetime minus 60s leeway. Refresh on 401.
|
||||
|
||||
**Payload shape:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Items": [
|
||||
{ "SerialNumber": "172789-7", "Content": " DATAFORTH CORPORATION ..." },
|
||||
{ "SerialNumber": "172789-8", "Content": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Batches of 100 items per HTTP request. Each request has a 120s timeout. HTTP errors and timeouts logged, but the upload path is **non-throwing when called from import.js** — a Hoffman outage or transient failure must not wedge the import flow.
|
||||
|
||||
**Response shape:**
|
||||
|
||||
```json
|
||||
{
|
||||
"TotalReceived": 100,
|
||||
"Created": 98,
|
||||
"Updated": 0,
|
||||
"Unchanged": 0,
|
||||
"Errors": [ "SN=12345-7: validation failed: ..." ]
|
||||
}
|
||||
```
|
||||
|
||||
**Post-response bookkeeping:** for every SN that was *not* in the `Errors` list, the DB stamps `api_uploaded_at = NOW()`. This is what drives the "on website" indicator in the dashboard.
|
||||
|
||||
**Upload triggers — three paths:**
|
||||
|
||||
1. **Automatic on import.** After `import.js` completes a batch insert, it calls `uploadNewRecords(filePaths)` which picks up PASS records from the just-imported files and pushes them.
|
||||
2. **Manual per-record.** The web dashboard shows a PUSH button on every row; clicking it calls `POST /api/upload { ids: [123] }` which resolves to SNs and pushes.
|
||||
3. **Bulk push.** Dashboard has a "PUSH TO WEB" button that pushes all selected records or (internally) `all_unuploaded=true` to re-run every unpushed PASS.
|
||||
|
||||
A fourth safety net — the **daily scheduled task** at 02:30 — runs `C:\ProgramData\dataforth-uploader\run-pipeline.ps1` as SYSTEM to catch anything that slipped through real-time uploads. Introduced as a fallback while the real-time path was stabilizing; now mostly informational.
|
||||
|
||||
---
|
||||
|
||||
## 4. Dashboard User Interface
|
||||
|
||||
**URL:** `http://192.168.0.6:3000/` (internal LAN only)
|
||||
|
||||
### 4.1 Search and filters
|
||||
|
||||
Left panel provides:
|
||||
|
||||
- **Serial Number** — LIKE-match any substring
|
||||
- **Work Order #**
|
||||
- **Model Number** — LIKE-match
|
||||
- **Result** — All / Pass Only / Fail Only
|
||||
- **Product Line** — dropdown populated from available `log_type` values
|
||||
- **Website Status** — **Any / On Website / Not on Website** (new 2026-04-15)
|
||||
- **Advanced:** Test Station, Date range, Full-text search in raw_data
|
||||
|
||||
### 4.2 Result rows
|
||||
|
||||
Each row shows Serial, Model, Date, Station, Product, Result, Actions. Visual cues:
|
||||
|
||||
- **Pink tint** — record is not on the Dataforth website (`api_uploaded_at IS NULL`)
|
||||
- **Normal styling** — record is live on the website
|
||||
- Tooltip on mouse-over explains status
|
||||
|
||||
### 4.3 Row actions
|
||||
|
||||
- **VIEW** — expand full record detail modal
|
||||
- **SHEET** — preview the rendered datasheet (same text that was/would be sent to the website)
|
||||
- **PUSH / RE-PUSH** — send this record to the website. Label says "PUSH" for records never uploaded, "RE-PUSH" for records already on the website (updates the stored copy).
|
||||
- Button disabled for records where `overall_result != 'PASS'` (website only accepts PASS units, by policy)
|
||||
|
||||
### 4.4 Bulk actions
|
||||
|
||||
- Checkbox on each row + **Select Page** checkbox
|
||||
- **PUSH TO WEB** bulk button — pushes every selected record in one API call
|
||||
|
||||
### 4.5 Export
|
||||
|
||||
- **EXPORT CSV** — exports the current filtered result set as CSV for offline review
|
||||
|
||||
---
|
||||
|
||||
## 5. Operational Notes
|
||||
|
||||
### 5.1 Service and account context
|
||||
|
||||
- Service name: `testdatadb`
|
||||
- Service wrapper: WinSW (`C:\Shares\testdatadb\daemon\testdatadb.exe`)
|
||||
- Run-as account: `INTRANET\svc_testdatadb`
|
||||
- Auto-start: Automatic (Delayed Start recommended but not required)
|
||||
- Logs: `C:\Shares\testdatadb\logs\testdatadb.out.log`, `.err.log`, `.wrapper.log`
|
||||
|
||||
### 5.2 Credentials
|
||||
|
||||
- **File:** `C:\ProgramData\dataforth-uploader\credentials.json` (JSON with `CF_TOKEN_URL`, `CF_API_BASE`, `CF_CLIENT_ID`, `CF_CLIENT_SECRET`, `CF_SCOPE`)
|
||||
- **ACL:** SYSTEM (FullControl), Administrators (FullControl), `INTRANET\svc_testdatadb` (Read). Nobody else.
|
||||
- **Source of truth:** same file used by both the testdatadb real-time path and the daily scheduled task fallback. Never duplicate creds into the service config.
|
||||
|
||||
### 5.3 Database connection
|
||||
|
||||
- Local PostgreSQL on AD2
|
||||
- Environment variables (with defaults):
|
||||
- `PGHOST=localhost`
|
||||
- `PGPORT=5432`
|
||||
- `PGUSER=testdatadb_app`
|
||||
- `PGDATABASE=testdatadb`
|
||||
- `PGPASSWORD` — set in service config
|
||||
|
||||
### 5.4 Key file locations on AD2
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `C:\Shares\testdatadb\database\import.js` | Parses .dat files, inserts to DB |
|
||||
| `C:\Shares\testdatadb\database\upload-to-api.js` | Pushes records to Hoffman |
|
||||
| `C:\Shares\testdatadb\database\render-datasheet.js` | In-memory datasheet rendering |
|
||||
| `C:\Shares\testdatadb\database\export-datasheets.js` | Legacy For_Web writer (retained for compat) |
|
||||
| `C:\Shares\testdatadb\routes/api.js` | HTTP API (search, upload, datasheet preview) |
|
||||
| `C:\Shares\testdatadb\public\index.html` | Dashboard UI |
|
||||
| `C:\Shares\testdatadb\parsers\` | `.dat` binary parsers per log type |
|
||||
| `C:\Shares\testdatadb\specdata\` | QuickBASIC spec files (5BMAIN.DAT, 7BMAIN.DAT, etc.) |
|
||||
| `C:\Shares\testdatadb\templates\datasheet-exact.js` | Formatter that replicates the QuickBASIC output |
|
||||
| `C:\Shares\webshare\For_Web\` | Legacy intermediate output directory |
|
||||
| `C:\ProgramData\dataforth-uploader\` | Scheduled task payload + credentials |
|
||||
|
||||
---
|
||||
|
||||
## 6. Record State Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
Incoming .dat record │
|
||||
│ │
|
||||
│ parse, insert (ON CONFLICT (serial_number)) │
|
||||
▼ │
|
||||
┌────────────┐ re-test, new test_date newer │
|
||||
│ In DB │◀─────────────────────────────────────┐ │
|
||||
│ PASS/FAIL │ │ │
|
||||
└─────┬──────┘ │ │
|
||||
│ │ │
|
||||
PASS? ──┴── FAIL? │ │
|
||||
│ │ │ │
|
||||
│ └─▶ Stays internal, never uploaded ──────┘ │
|
||||
│ (can become PASS on retest) │
|
||||
│ │
|
||||
▼ │
|
||||
render via render-datasheet.js │
|
||||
│ │
|
||||
│ specs missing? ──▶ skipped; tracked as "unpushable" ─────┘
|
||||
│
|
||||
▼
|
||||
upload-to-api.js → POST /bulk
|
||||
│
|
||||
│ Hoffman response
|
||||
▼
|
||||
┌───────────────┬──────────────┬──────────────┐
|
||||
│ Created │ Unchanged │ Errors │
|
||||
│ │ │ │
|
||||
│ stamp │ stamp │ no stamp; │
|
||||
│ api_uploaded │ api_uploaded │ tracked in │
|
||||
│ _at = NOW() │ _at = NOW() │ logs; retry │
|
||||
└───────┬───────┴──────┬───────┴──────┬───────┘
|
||||
│ │ │
|
||||
└──────────┬───┘ │
|
||||
│ │
|
||||
▼ ▼
|
||||
ON DATAFORTH WEBSITE NOT ON WEBSITE
|
||||
(pink tint off) (pink tint on, PUSH button live)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting & FAQ
|
||||
|
||||
### 7.1 "A unit I know passed isn't showing up on the website"
|
||||
|
||||
1. Search the dashboard for the SN. Is it in testdatadb at all?
|
||||
- **Not in DB:** the import hasn't picked up that file. Check the source `.dat` path and the import logs. Re-run the import manually if needed.
|
||||
- **In DB but `overall_result=FAIL`:** by engineering policy, FAILs don't go to the website. If the unit has been retested and passes, make sure the retest `.dat` file has been imported — the FAIL→PASS rule will then update the record.
|
||||
2. **In DB with `overall_result=PASS`, `api_uploaded_at=NULL`:** the record hasn't been pushed. Check the dashboard: hover on PUSH button for tooltip. If it says "Only PASS records can be pushed", the row is FAIL (verify). Otherwise click PUSH manually.
|
||||
3. **Push attempt fails with "skipped"** in service logs: the model has no spec file, so we can't render the datasheet. Either the spec data is missing from `C:\Shares\testdatadb\specdata\` or this is a new model variant that hasn't been added to the spec files. **Action:** get the spec data from engineering and drop into the specdata folder; restart testdatadb service; record will then be pushable.
|
||||
4. **Push attempt fails with Hoffman error:** check logs. Usually a data quality issue (malformed field, missing required measurement). Fix the source record or accept that this specific unit won't be on the website.
|
||||
|
||||
### 7.2 "A unit is on the website but shouldn't be"
|
||||
|
||||
By engineering policy, only PASS units should be on the website. If a FAIL has made it:
|
||||
|
||||
1. Look up the SN history on Hoffman's API. Most likely the unit passed a prior test and was pushed; later failure won't retroactively remove it.
|
||||
2. If engineering wants the record retracted from the website, that's a direct call to Hoffman's API — outside the testdatadb pipeline.
|
||||
|
||||
We also observed about 8% of our local FAIL records *are* on Hoffman. In every sampled case this is the legitimate pattern: unit passed originally (pushed), was later re-tested and failed (kept locally as FAIL), but the original PASS datasheet is still on the website. If the unit needs retraction, do it at the website end.
|
||||
|
||||
### 7.3 "The dashboard shows counts that don't match Hoffman"
|
||||
|
||||
The local dashboard shows our DB state. Hoffman's site contains our local set **plus a large pre-testdatadb historical set** (202,866 records as of 2026-04-15) that was uploaded by prior tools and is not reproducible from our current DB. This is expected — don't try to reconcile the two totals; check specific SNs instead.
|
||||
|
||||
### 7.4 "How do I force a re-push of an already-on-web record?"
|
||||
|
||||
Click **RE-PUSH** on the row. This sends the record to Hoffman again — useful if:
|
||||
|
||||
- The local record was corrected and you want the website copy updated
|
||||
- You suspect the website copy is wrong
|
||||
- Engineering asked for a refresh
|
||||
|
||||
Hoffman returns `Unchanged` if the content is identical — so RE-PUSH is idempotent and safe to run casually.
|
||||
|
||||
### 7.5 "How do I check if a specific SN is on the website?"
|
||||
|
||||
Three options:
|
||||
|
||||
1. **Dashboard:** Search for the SN. Row styling + tooltip shows status.
|
||||
2. **Direct API:** `GET https://{CF_API_BASE}/api/v1/TestReportDataFiles/{SN}` with a bearer token. Returns 200 + Content if on website, 404 if not.
|
||||
3. **Public product page:** navigate to the Dataforth product page for that SN; if the datasheet link works, it's on the site.
|
||||
|
||||
---
|
||||
|
||||
## 8. Known Limitations
|
||||
|
||||
- **No real-time filesystem watcher.** The import currently runs on a schedule (or manual trigger). Records appear in the DB minutes after tests complete, not seconds. Good enough for business purposes.
|
||||
- **VASLOG_ENG files are shipped verbatim** — the customer-engineering variant has non-standard formatting that varies per customer. We store and push the original file contents rather than reformatting. Implication: the website datasheet for these units matches exactly what engineering produced, no template regeneration.
|
||||
- **Spec data is QuickBASIC binary.** New product families or variants require dropping updated `.DAT` spec files into `C:\Shares\testdatadb\specdata\`. The modern Node.js code reads the QB binary format directly — no conversion needed — but the files themselves still have to come from the legacy spec authoring tools (or be manually edited with a binary-aware editor).
|
||||
- **Pre-testdatadb historical records.** The ~203K units that live on Hoffman from the DFWDS era don't exist in our DB. We can't re-render those datasheets from current state. If one of those legacy records has a data issue, the fix needs to be made directly at Hoffman.
|
||||
|
||||
---
|
||||
|
||||
## 9. Recent Changes (2026-04-15 Release)
|
||||
|
||||
For context on what's new if engineering has seen earlier documentation:
|
||||
|
||||
1. **Database deduplicated** — one row per serial number (was permitting multiple). Kept the best record per SN by state-priority (on-web > on-filesystem > newer test date).
|
||||
2. **Unique constraint on `serial_number`** added and enforced.
|
||||
3. **FAIL→PASS retest rule** formalized in the `ON CONFLICT` logic. Previously a FAIL record could linger forever even after a successful retest; now the retest correctly replaces it.
|
||||
4. **For_Web filesystem dependency eliminated** for the upload path. Datasheets render in memory directly from the DB. Phantom `forweb_exported_at` stamps (rows that claimed a file existed but the file was gone) are no longer a source of push failures.
|
||||
5. **Dashboard UI upgrades:**
|
||||
- Row coloring indicates on-website status at a glance
|
||||
- Per-row PUSH / RE-PUSH buttons
|
||||
- Bulk PUSH TO WEB button
|
||||
- Website Status filter (Any / On Website / Not on Website)
|
||||
6. **Manual push end-to-end tested** — 170,984 records (mostly SCM7B + DSCT that were historically never pushed) successfully uploaded to Hoffman in this release window.
|
||||
|
||||
---
|
||||
|
||||
## 10. Contacts
|
||||
|
||||
- **System owner / technical questions:** Mike Swanson, AZ Computer Guru, mike@azcomputerguru.com, (520) 304-8300
|
||||
- **Dashboard URL (internal):** http://192.168.0.6:3000/
|
||||
- **Logs (internal):** `\\ad2\c-drive\Shares\testdatadb\logs\`
|
||||
- **Escalation for Hoffman-side issues:** Dataforth web/IT (they own the product API endpoint)
|
||||
@@ -0,0 +1,585 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Domain Ownership Attestation — Arizona Computer Guru, LLC</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+SC:wght@500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Cormorant+Unicase:wght@500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ink: #14192b;
|
||||
--ink-soft: #27324f;
|
||||
--paper: #fbf8f1;
|
||||
--paper-deep: #f4efe3;
|
||||
--accent: #7a5a1e;
|
||||
--accent-soft: #b2913f;
|
||||
--rule: #2a3455;
|
||||
--muted: #6a6455;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: letter;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
html, body { background: var(--paper) !important; }
|
||||
.sheet { box-shadow: none !important; margin: 0 !important; border: none !important; }
|
||||
.screen-only { display: none !important; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #3a3a3a;
|
||||
color: var(--ink);
|
||||
font-family: 'EB Garamond', Georgia, serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 40px 20px 60px;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: relative;
|
||||
width: 8.5in;
|
||||
min-height: 11in;
|
||||
margin: 0 auto;
|
||||
padding: 0.9in 0.85in 0.85in;
|
||||
background: var(--paper);
|
||||
background-image:
|
||||
radial-gradient(ellipse at top left, rgba(178,145,63,0.04), transparent 40%),
|
||||
radial-gradient(ellipse at bottom right, rgba(42,52,85,0.035), transparent 45%),
|
||||
linear-gradient(var(--paper), var(--paper));
|
||||
box-shadow:
|
||||
0 1px 0 rgba(20,25,43,0.08),
|
||||
0 28px 60px -20px rgba(20,25,43,0.35),
|
||||
0 10px 30px -10px rgba(20,25,43,0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Subtle paper grain */
|
||||
.sheet::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' seed='3'/%3E%3CfeColorMatrix values='0 0 0 0 0.08 0 0 0 0 0.05 0 0 0 0 0.02 0 0 0 0.045 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
mix-blend-mode: multiply;
|
||||
opacity: 0.6;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Watermark monogram behind content */
|
||||
.sheet::after {
|
||||
content: "ACG";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-8deg);
|
||||
font-family: 'Cormorant Unicase', serif;
|
||||
font-weight: 600;
|
||||
font-size: 360pt;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--rule);
|
||||
opacity: 0.025;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ========== Masthead ========== */
|
||||
.masthead {
|
||||
text-align: center;
|
||||
padding-bottom: 0.3in;
|
||||
margin-bottom: 0.35in;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mark {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 4px 18px;
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--accent);
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 8pt;
|
||||
letter-spacing: 0.45em;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.firm-name {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
font-size: 30pt;
|
||||
letter-spacing: 0.18em;
|
||||
line-height: 1.05;
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.firm-name .amp {
|
||||
color: var(--accent);
|
||||
font-style: italic;
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: var(--muted);
|
||||
font-size: 11pt;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
/* Double rule ornament */
|
||||
.rule-ornament {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
margin: 4px 0 14px;
|
||||
}
|
||||
.rule-ornament .bar {
|
||||
flex: 1;
|
||||
max-width: 28%;
|
||||
height: 0;
|
||||
border-top: 1.4pt double var(--rule);
|
||||
}
|
||||
.rule-ornament .diamond {
|
||||
color: var(--accent);
|
||||
font-size: 8pt;
|
||||
letter-spacing: 0.3em;
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.contact-row {
|
||||
font-family: 'EB Garamond', serif;
|
||||
font-size: 10.5pt;
|
||||
color: var(--ink-soft);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.contact-row .sep {
|
||||
color: var(--accent);
|
||||
margin: 0 10px;
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-size: 11pt;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ========== Document body ========== */
|
||||
.doc-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin: 0.3in 0 0.25in;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.case-ref {
|
||||
flex: 1;
|
||||
border-left: 2px solid var(--accent);
|
||||
padding: 4px 0 4px 14px;
|
||||
}
|
||||
|
||||
.case-ref .kicker {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 8pt;
|
||||
letter-spacing: 0.35em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.case-ref .title {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-weight: 500;
|
||||
font-size: 12pt;
|
||||
color: var(--ink);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.case-ref .id {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-size: 10pt;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.date-block {
|
||||
flex: 0 0 auto;
|
||||
text-align: right;
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-style: italic;
|
||||
font-size: 11.5pt;
|
||||
color: var(--ink-soft);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.salutation {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-size: 12pt;
|
||||
color: var(--ink);
|
||||
margin: 0.3in 0 0.2in;
|
||||
}
|
||||
|
||||
.body-copy {
|
||||
font-family: 'EB Garamond', serif;
|
||||
font-size: 12pt;
|
||||
line-height: 1.65;
|
||||
color: var(--ink);
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
}
|
||||
|
||||
.body-copy p {
|
||||
margin: 0 0 12pt;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.body-copy p:first-of-type::first-letter {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-weight: 500;
|
||||
font-size: 34pt;
|
||||
line-height: 0.9;
|
||||
float: left;
|
||||
padding: 8px 10px 0 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.body-copy strong {
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.body-copy em {
|
||||
font-style: italic;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
/* Quoted claim block */
|
||||
.asserts {
|
||||
margin: 14pt 0 14pt;
|
||||
padding: 8pt 20pt 2pt;
|
||||
border-top: 1px solid var(--paper-deep);
|
||||
border-bottom: 1px solid var(--paper-deep);
|
||||
background: linear-gradient(var(--paper-deep), transparent);
|
||||
}
|
||||
.asserts ol {
|
||||
margin: 0;
|
||||
padding-left: 22px;
|
||||
font-family: 'EB Garamond', serif;
|
||||
font-size: 11.5pt;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.asserts ol li {
|
||||
padding: 6pt 0;
|
||||
text-align: justify;
|
||||
color: var(--ink);
|
||||
}
|
||||
.asserts ol li::marker {
|
||||
color: var(--accent);
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* ========== Signature ========== */
|
||||
.signature {
|
||||
margin-top: 0.45in;
|
||||
}
|
||||
|
||||
.closing {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-style: italic;
|
||||
font-size: 12pt;
|
||||
color: var(--ink-soft);
|
||||
margin: 0 0 0.75in;
|
||||
}
|
||||
|
||||
.sig-block {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.sig-left {
|
||||
flex: 1;
|
||||
max-width: 3.2in;
|
||||
}
|
||||
|
||||
.sig-line {
|
||||
border-top: 1.2pt solid var(--ink);
|
||||
padding-top: 6pt;
|
||||
}
|
||||
|
||||
.sig-name {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 11pt;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--ink);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sig-title {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-style: italic;
|
||||
font-size: 10.5pt;
|
||||
color: var(--muted);
|
||||
margin-top: 2pt;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.seal {
|
||||
flex: 0 0 auto;
|
||||
width: 1.4in;
|
||||
height: 1.4in;
|
||||
border-radius: 50%;
|
||||
border: 1.6pt double var(--accent);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: radial-gradient(circle at center, rgba(178,145,63,0.08), transparent 70%);
|
||||
}
|
||||
|
||||
.seal::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 4pt;
|
||||
border-radius: 50%;
|
||||
border: 0.6pt solid var(--accent-soft);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.seal .seal-top {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 6.5pt;
|
||||
letter-spacing: 0.25em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.seal .seal-mark {
|
||||
font-family: 'Cormorant Unicase', serif;
|
||||
font-weight: 600;
|
||||
font-size: 24pt;
|
||||
line-height: 1;
|
||||
color: var(--ink);
|
||||
margin: 4pt 0 2pt;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.seal .seal-bottom {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 5.5pt;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ========== Footer ========== */
|
||||
.colophon {
|
||||
margin-top: 0.5in;
|
||||
padding-top: 10pt;
|
||||
border-top: 0.5pt solid var(--paper-deep);
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 500;
|
||||
font-size: 7pt;
|
||||
letter-spacing: 0.28em;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.colophon .divider {
|
||||
color: var(--accent);
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* ========== Screen-only print button ========== */
|
||||
.print-bar {
|
||||
max-width: 8.5in;
|
||||
margin: 0 auto 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.print-bar button {
|
||||
font-family: 'Cormorant SC', serif;
|
||||
font-weight: 600;
|
||||
font-size: 10pt;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
padding: 10px 22px;
|
||||
background: #fbf8f1;
|
||||
color: #14192b;
|
||||
border: 1px solid #14192b;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
box-shadow: 2px 2px 0 #7a5a1e;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
.print-bar button:hover {
|
||||
transform: translate(-1px, -1px);
|
||||
box-shadow: 3px 3px 0 #7a5a1e;
|
||||
}
|
||||
.print-bar button:active {
|
||||
transform: translate(1px, 1px);
|
||||
box-shadow: 0 0 0 #7a5a1e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="print-bar screen-only">
|
||||
<button onclick="window.print()">Print / Save as PDF</button>
|
||||
</div>
|
||||
|
||||
<article class="sheet" role="document">
|
||||
<div class="content">
|
||||
|
||||
<!-- ===== Masthead ===== -->
|
||||
<header class="masthead">
|
||||
<div class="mark">Established 2001</div>
|
||||
<h1 class="firm-name">Arizona Computer Guru<span class="amp">,</span> LLC</h1>
|
||||
<p class="tagline">Information Technology & Systems Engineering · Tucson, Arizona</p>
|
||||
<div class="rule-ornament">
|
||||
<span class="bar"></span>
|
||||
<span class="diamond">❦</span>
|
||||
<span class="bar"></span>
|
||||
</div>
|
||||
<div class="contact-row">
|
||||
7437 East 22nd Street, Tucson, Arizona 85710
|
||||
<span class="sep">❦</span>
|
||||
(520) 304-8300
|
||||
<span class="sep">❦</span>
|
||||
azcomputerguru.com
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ===== Document meta ===== -->
|
||||
<section class="doc-meta">
|
||||
<div class="case-ref">
|
||||
<div class="kicker">Re</div>
|
||||
<div class="title">Domain Ownership Attestation — Azure Trusted Signing Identity Validation</div>
|
||||
<div class="id">Identity Validation ID · 03028768-f611-4904-aa58-c755020f436a</div>
|
||||
</div>
|
||||
<div class="date-block">
|
||||
Fifteenth day of April,<br>two thousand twenty-six
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Body ===== -->
|
||||
<p class="salutation"><strong>To the Microsoft Identity Validation Review Team:</strong></p>
|
||||
|
||||
<div class="body-copy">
|
||||
<p>
|
||||
I, the undersigned, do hereby attest and affirm in my official capacity as <strong>President of
|
||||
Arizona Computer Guru, LLC</strong>, a limited liability company in good standing organized under
|
||||
the laws of the State of Arizona, that the matters set forth in this letter are true, complete, and
|
||||
correct to the best of my knowledge and belief, and are made in connection with the identity
|
||||
validation request referenced above for the purpose of obtaining public-trust artifact signing
|
||||
certificates through Microsoft’s Azure Trusted Signing service.
|
||||
</p>
|
||||
|
||||
<div class="asserts">
|
||||
<ol>
|
||||
<li>
|
||||
The internet domain name <strong>azcomputerguru.com</strong> is owned by and
|
||||
used exclusively for the business operations of <strong>Arizona Computer Guru, LLC</strong>
|
||||
(the “Company”). The domain is registered with GoDaddy.com, LLC and is held of
|
||||
record in the name of the undersigned in the undersigned’s capacity as President and
|
||||
authorized representative of the Company.
|
||||
</li>
|
||||
<li>
|
||||
The Company’s public records are consistent with this attestation. The business
|
||||
address, telephone number, and corporate identifiers submitted in the Azure Trusted
|
||||
Signing Identity Validation request correspond to the Company’s registered address
|
||||
and principal place of business, and to the undersigned’s authority to act for the
|
||||
Company.
|
||||
</li>
|
||||
<li>
|
||||
The undersigned is authorized to make this attestation on behalf of the Company and to
|
||||
bind the Company to the representations made herein for the limited purpose of the
|
||||
identity validation review described above.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
A copy of the GoDaddy renewal invoice for <em>azcomputerguru.com</em> is provided as supporting
|
||||
documentation; the billing address, contact telephone, and registered domain on that receipt
|
||||
correspond precisely to the Company identifiers set forth in this attestation.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Should Microsoft require additional documentation or clarification in connection with this
|
||||
validation request, please contact the undersigned directly using the address and telephone set
|
||||
forth in the masthead of this letter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ===== Signature ===== -->
|
||||
<div class="signature">
|
||||
<p class="closing">Respectfully submitted,</p>
|
||||
<div class="sig-block">
|
||||
<div class="sig-left">
|
||||
<img src="mikesig.png" alt="Signature of Michael Swanson"
|
||||
style="display:block; height:0.85in; width:auto; margin-bottom:-6pt; object-fit:contain; filter:saturate(0) contrast(1.4) brightness(0.2); mix-blend-mode:multiply;">
|
||||
<div class="sig-line">
|
||||
<div class="sig-name">Michael Swanson</div>
|
||||
<div class="sig-title">President · Arizona Computer Guru, LLC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="seal" aria-hidden="true">
|
||||
<div class="seal-top">Arizona LLC</div>
|
||||
<div class="seal-mark">ACG</div>
|
||||
<div class="seal-bottom">Est. 2001</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Colophon ===== -->
|
||||
<footer class="colophon">
|
||||
Arizona Limited Liability Company
|
||||
<span class="divider">❦</span>
|
||||
Federal EIN 20-5419777
|
||||
<span class="divider">❦</span>
|
||||
D-U-N-S 00-566-1506
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
projects/msp-tools/guru-rmm/signing-attestation/mikesig.jpg
Normal file
BIN
projects/msp-tools/guru-rmm/signing-attestation/mikesig.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
projects/msp-tools/guru-rmm/signing-attestation/mikesig.png
Normal file
BIN
projects/msp-tools/guru-rmm/signing-attestation/mikesig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
Reference in New Issue
Block a user