Skip to content

Prd2 af modernized#12

Closed
monigarr wants to merge 103 commits into
masterfrom
prd2_af_modernized
Closed

Prd2 af modernized#12
monigarr wants to merge 103 commits into
masterfrom
prd2_af_modernized

Conversation

@monigarr

@monigarr monigarr commented May 8, 2026

Copy link
Copy Markdown
Owner

Short description of what this changes or resolves:

Changes proposed in this pull request:

Was an AI assistant used? Yes / No

Monica Peters and others added 30 commits April 27, 2026 21:06
…: review audit the test coverage relevance accuracy
…d openai responses, formatted token usage on the visual card
AgentForge PRD1 new module created and manually visually tested. todo: review...

See merge request monicapeters/openemr!1
#### Short description of what this changes or resolves:
While working on openemr#11805 I came across a bug when running a SQL upgrade.
Invalid syntax! Since the block was conditional, you wouldn't always hit
it. The Inferno data did.

#### Changes proposed in this pull request:
Splits the index additions into a syntactically-legal version without
changing the results.

Well, technically the new conditionals could change the results, but the
intent of what it did before was clear.

#### Was an AI assistant used? Yes / No
No
Correction from openemr#11866, which was incorrectly extracted from openemr#11805.
…nemr#11857)

Refs openemr#11725.

## Summary

Diagnostics-only change motivated by the research in openemr#11725. The
Selenium standalone-chromium container occasionally returns `invalid
session id: session deleted as the browser has closed the connection`
mid-test, but the existing post-test `dc logs selenium` step has shown
no OOM kill, no tab-crashed event, and no supervisord restart at the
moment of failure. We can't pin down the silent Chrome death without
more evidence, so this PR makes the next failure leave a usable trace.

Adds a failure-only step in the e2e job in `.github/workflows/test.yml`
that writes the following to a `selenium-diagnostics/` directory and
uploads it as a separate artifact:

- `dmesg | tail -200` from the runner (OOM-killer / segfault lines)
- `docker compose top selenium` (process tree inside the container at
teardown)
- `docker stats --no-stream selenium` (memory/CPU at teardown)
- Full `docker compose logs selenium` written to a file (also still
echoed to the job log by the existing step, but the file is downloadable
alongside the rest)

No test behavior change. No image bumps, no `shm_size`, no restart
policies. The new steps are gated on `failure() && matrix.suite ==
'e2e'` so green builds are unaffected.

## Test plan

- [ ] Merge and wait for the next e2e flake on master
- [ ] On failure, download the `selenium-diagnostics-<docker_dir>`
artifact from the run
- [ ] Inspect `dmesg.tail.txt`, `docker-compose-top.txt`,
`docker-stats.txt`, and `selenium.log` for OOM-killer / segfault /
resource-pressure signals around the failure timestamp
- [ ] Feed findings back into openemr#11725 to choose the actual fix
…ker/development-insane in the openemr-images group across 1 directory (openemr#11870)

Bumps the openemr-images group with 1 update in the
/docker/development-insane directory: openemr/openemr.

Updates `openemr/openemr` from flex-3.17 to flex-3.17


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=openemr/openemr&package-manager=docker_compose&previous-version=flex-3.17&new-version=flex-3.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
….notFound (openemr#11859)

Drain `class.notFound` and `method.notFound` baseline entries
originating in the Carecoordination zend module. Fourth phase-4 module
batch following the eRx drain (openemr#11825).

## Source changes

**Controller property native types.** Convert PHPDoc-typed properties on
`CarecoordinationController` and `CcdController` to native types
(constructor property promotion applied via Rector). PHPStan was
resolving docblocks like `@var Documents\Controller\DocumentsController`
relative to the `Carecoordination\Controller` namespace, producing
`class.notFound` for nonexistent paths.

**`@method` tags for Laminas MVC plugins.** `$this->CommonPlugin()` and
`$this->Documents()` resolve through Laminas' plugin manager via
`__call`. Add explicit `@method` declarations on the controllers that
invoke them, plus `@method \Laminas\Http\Request getRequest()` to narrow
the request type so `getPost()`/`getQuery()` resolve.

**Fix `CcdController::uploadAction()` bug.** The method was calling
`$this->updateDocumentCategoryUsingCatname()` on itself, which does not
exist on the controller. The sibling
`CarecoordinationController::uploadAction()` calls it correctly via
`$this->documentsController->getDocumentsTable()->updateDocumentCategoryUsingCatname()`
— apply the same call here. Dead since at least 2014; PHPStan would have
caught it as `method.notFound` if the entry hadn't been baselined.

**Remove dead `CarecoordinationTable::getIssues()`.** Body called
`DocumentsTable::getIssues()` which does not exist. Author left an
inline `Method not used` TODO; grep confirms no callers.

**Remove dead try/catch in
`EncounterccdadispatchController::indexAction()`.** The block wrapped
only `echo`/`exit` calls that cannot throw.

**`getCarecoordinationTable()` / `getDocumentsTable()` return types.**
Changing the docblock from `@return object` to a real return type
collapses several `method.notFound` entries that were `Cannot call
method ... on object`.

**DOM narrowing.** `CcdaUserPreferencesTransformer` and
`CcdaServiceDocumentRequestorTest` iterate `\DOMNodeList` results and
call element-only methods (`insertBefore`, `setAttribute`,
`getAttribute`). Add `instanceof \DOMElement` and `instanceof \DOMNode`
guards plus a null-check on `lastElementChild` so the type system sees
the narrowing.

**Content type narrowing in
`EncounterccdadispatchController::indexAction()`.** Initialize `$content
= ''` up front and call `strval($content)` before substring operations
to avoid Rector flip-flop between `RecastingRemovalRector`
(full-codebase) and `NullToStrictStringFuncCallArgRector` (single-file).

## Tests

Adds `CcdaUserPreferencesTransformerTest` covering `transform()` (sort,
truncate, default-fallback) and the getter/setter round-trip. Lifts
patch coverage on this PR.

## Counts

| Identifier | Before | After | Delta |
|---|---:|---:|---:|
| `class.notFound` | 237 | 229 | -8 |
| `method.notFound` | 184 | 160 | -24 |
| `method.nonObject` | 1995 | 1991 | -4 |
| `argument.type` | 14973 | 14972 | -1 |
| `assign.propertyType` | 388 | 384 | -4 |
| `cast.string` | 807 | 806 | -1 |
| `missingType.parameter` | 9488 | 9487 | -1 |
| `missingType.property` | 6198 | 6195 | -3 |
| `missingType.return` | 5913 | 5912 | -1 |
| `offsetAccess.nonOffsetAccessible` | 10757 | 10756 | -1 |
| `preDec.type` | 9 | 8 | -1 |
| `property.notFound` | 135 | 132 | -3 |
| `return.type` | 2167 | 2164 | -3 |
| `variable.undefined` | 3075 | 3074 | -1 |

Caps in `.phpstan/fatal-baseline-caps.php` updated to match.

Refs openemr#11792.

## Test plan
- [x] `composer phpstan-baseline` regenerates cleanly
- [x] `composer phpunit-isolated -- --filter=FatalBaselineCaps` passes
- [x] `composer phpcs` clean on edited files
- [x] Pre-commit hooks (rector, phpcs, phpstan, codespell,
composer-require-checker) all pass
…_id (openemr#11618)

## Summary

- Replace `die("Site ID is missing from session data!")` in
`globals.php` with `throw new MissingSiteIdException()`
- Introduce `MissingSiteIdException` extending the existing
`MissingSiteException`, so callers can catch either the specific session
case or the broader "site not identified" family
- This allows callers like `BackgroundServiceRunner` that wrap
`globals.php` inclusion in `try`/`catch` to handle the failure
gracefully and release DB locks, instead of having the process killed
mid-execution

## Notes

`portal/patient/scripts/app.js:30` checks for the string "Site ID is
missing from session data" in AJAX responses. That handler relies on
parsing the raw `die()` output, which is already fragile. With this
change, uncaught exceptions in production won't produce that exact
string in the response body (PHP returns a 500 with no body when
`display_errors` is off). The JS handler should be updated separately to
use a proper error response format.

## Test plan

- [ ] Verify web requests without a session site_id still get a 400
response (the `http_response_code(400)` call is preserved before the
throw)
- [ ] Verify CLI callers (e.g. `BackgroundServiceRunner`) can catch
`MissingSiteIdException` and clean up gracefully
- [ ] Verify the patient portal session expiry flow still redirects
correctly (may need follow-up for `app.js`)

Closes openemr#11616

---------

Co-authored-by: Eric Stern <eric@ericstern.com>
…10387)

## Summary

Fixes openemr#10386

PHPStan's container cache (Nette DI) validates against file modification
times. Git doesn't preserve mtimes, so a freshly checked out file always
looks newer than its cached metadata, invalidating the entire container
and forcing a full re-analysis of all ~4000 files even when the result
cache restored cleanly.

## Approach

The expensive parts of fixing this are (1) a full-history clone, which
`git-restore-mtime` needs to walk, and (2) installing the tool itself.
Both are pure waste on a cold cache miss. So this PR makes them
conditional on whether a cache was actually restored:

- Replace `actions/cache@v5` with `actions/cache/restore@v5` +
`actions/cache/save@v5` so the restore step exposes a `cache-hit` /
`cache-matched-key` output.
- When (and only when) a cache was restored: install `git-restore-mtime`
from apt, `git fetch --unshallow`, and walk the history to restore
mtimes.
- Save the cache on `always()` so partial results from a failed analyze
still seed the next run.

Cold misses pay zero new overhead vs. before this PR. Warm hits pay an
unshallow + mtime walk to make the restored cache validate.

## Testing

The first run after merge is still cold-cache (no savings). Compare
runtime on the second and third runs of the same branch to confirm the
cache is now effective.
…undefined) (openemr#11878)

## Summary

Refs openemr#11792.

Drains 19 `method.notFound` and 10 `variable.undefined` baseline entries
in `library/edihistory/` without changing runtime behavior.

- Type `csv_check_x12_obj()` return as `edih_x12_file|false` so PHPStan
can narrow callers across the 11 sibling html/error/segments files.
Replace the now-redundant `'edih_x12_file' == \$obj::class` checks with
`\$obj !== false`.
- Pre-initialize accumulator variables in the 271 and 277 segment loops
(`\$cls`, `\$loopid`, plus segment-specific scalars). Limited to files
where the variables are pure accumulators — `edih_997_error.php` etc.
use `isset()` as a "did this segment appear?" check, so pre-initializing
would alter behavior.
- Decrement caps in `.phpstan/fatal-baseline-caps.php`:
  - `method.notFound.php`: 160 → 141
  - `variable.undefined.php`: 3074 → 3064
- Regenerate `.phpstan/baseline/*.php` (side-effect drains in
`argument.type`, `booleanAnd.leftAlwaysTrue`,
`encapsedStringPart.nonString`, etc.).

## Test plan

- [x] `composer phpstan` passes (via pre-commit hook)
- [x] `composer phpunit-isolated -- --filter
FatalBaselineCapsIsolatedTest` — 19/19 pass
- [ ] CI green
…openemr#11877)

## Summary

Drains 45 entries from the PHPStan `class.notFound` fatal-category
baseline by correcting bogus PHPDoc `@var` and `@param` tokens in
portal/patient files. The cap is lowered from 237 to 192 in the same
commit.

Most fixes replace SQL types (`date`, `longtext`, `datetime`, `char`,
`blob`), English description words, and `unknown` with correct PHP
types. Editing `IDaoMap2::AddMap` / `SetFetchingStrategy` propagates via
`{@inheritdoc}` to every implementing Map class.

In `GlobalConfig::GetRenderEngine`, the dynamically instantiated render
engine is now narrowed via `assert($engine instanceof IRenderEngine)` so
the subsequent `assign()` / `display()` calls resolve against the
interface instead of plain `object` (avoiding new `method.notFound`
entries).

Refs openemr#11792.

Non-overlapping with the WIP in openemr#11859 (Carecoordination zend module).

## Test plan

- [x] `composer phpstan-baseline` regenerates clean
- [x] Pre-commit hooks pass (phpcs, phpstan, rector,
composer-require-checker)
- [ ] CI green, including `FatalBaselineCapsIsolatedTest`
…rm (openemr#11883)

## Summary

`C_EncounterVisitForm::render()` calls
`UuidRegistry::uuidToString($result['uuid'])` immediately after
`sqlQuery("SELECT * FROM form_encounter WHERE id = ?", [$id])` without
checking whether the row was found, or whether the `uuid` column was
populated. When the request carries a stale or invalid encounter id
(e.g. a deleted encounter via a bookmarked URL), or the row exists but
predates uuid backfill, `$result['uuid']` is `null` and
`Ramsey\Uuid\Uuid::fromBytes(null)` throws:

```
PHP Notice: TypeError: Ramsey\Uuid\Uuid::fromBytes(): Argument #1 ($bytes) must be of type string, null given,
called in src/Common/Uuid/UuidRegistry.php on line 287
Stack trace:
#0 src/Common/Uuid/UuidRegistry.php(287): Ramsey\Uuid\Uuid::fromBytes(NULL)
#1 interface/forms/newpatient/C_EncounterVisitForm.class.php(504): UuidRegistry::uuidToString(NULL)
#2 interface/forms/newpatient/common.php(43): C_EncounterVisitForm->render(2)
```

This change:
- Throws `RuntimeException` when no row matches, instead of silently
continuing with `$encounter = false` and crashing several lines later in
an unrelated stack frame.
- Passes `''` through to `$encounter['uuid']` when the row exists but
`uuid` is `null` (the comment on the original line notes this field is
set so the array JSON-encodes — empty string preserves that intent).
- Drops the `offsetAccess.nonOffsetAccessible` baseline entry that
covered the old unsafe access.

## Test plan

- [ ] View an existing encounter — renders normally
- [ ] Visit `view_form.php` with a deleted/invalid encounter id —
surfaces the explicit "Encounter not found" error rather than a
`fromBytes(null)` TypeError
- [ ] PHPStan passes (`composer phpstan`)

Assisted-by: Claude Code
#### Short description of what this changes or resolves:
The older-style `HelpfulDie` does not particularly live up to its name.
Fixing it the right way is removal in favor of everything using
exceptions, which is an ongoing but massive task. Debugging is overly
complex.

This logs the function and full stack trace any time the legacy
global-namespace SQL functions catch/suppress/crash any sort of query
failure. Yes, `HelpfulDie` makes an attempt to do this, but it's rather
limited in how it works and doesn't go through the SystemLogger.

I was hoping this would help with diagnosing some Inferno test issues.
It doesn't seem to, but I think it's still a worthwhile addition.

#### Changes proposed in this pull request:
Add an error-level log to all of the `catch` paths inside the older SQL
functions, going to the system logger.

#### Was an AI assistant used? Yes / No
No
…y\X12File (openemr#11879)

## Summary

Refs openemr#11792.

Lifts the procedural `edih_x12_file` class from
`library/edihistory/edih_x12file_class.php` into the modern PSR-4
namespace `OpenEMR\Billing\EdiHistory\X12File`, leaving a `class_alias`
shim so all existing legacy callers keep working.

This PR is **behavior-preserving** — no type tightening beyond what the
lift requires. Future PRs will tighten types and drain more baseline
entries on the lifted class.

## Changes

- Moves `edih_x12_file` class body into
`src/Billing/EdiHistory/X12File.php`
- Replaces `library/edihistory/edih_x12file_class.php` with a
`class_alias` shim
- Adds `.phpstan/phpstan_legacy_aliases.php` bootstrap so PHPStan
resolves the legacy `edih_x12_file` symbol (it does not follow runtime
`class_alias`)
- Adds fixture-based isolated tests under
`tests/Tests/Isolated/Billing/EdiHistory/X12FileIsolatedTest.php` (24
tests covering happy path, scan/delimiter/type/envelope error paths,
no-mk_segs path, and unknown-GS warning)
- Removes 81 bare `//` line comments inherited from the original
procedural class
- Regenerates the PHPStan baseline (fatal-cap counts unchanged)

## Test plan

- [x] `composer phpunit-isolated` passes
- [x] `composer phpstan` passes
- [x] CI green
…ity (openemr#11876)

## Summary

Follow-up to openemr#10762, which renamed the misspelled `declne_to_specfy`
race value to `decline_to_specify`. Brady's review on that PR flagged
two issues in `sql/8_0_0-to-8_1_0_upgrade.sql` (originally
`8_0_0-to-8_0_1_upgrade.sql`) that were never addressed before merge:

- The `patient_data.race` UPDATE was guarded by `#IfColumn patient_data
race`, which is always true — the UPDATE ran on every upgrade even when
there was nothing to convert. Switch to `#IfRow patient_data race
declne_to_specfy` so it's skipped when no rows match.
- The same misspelled value can also appear in `patient_data.language`
and `patient_data.ethnicity`. The `list_options` side already covers all
three list_ids; this PR mirrors that on the patient row side.

Refs openemr#10385, follow-up to
openemr#10762.

## Test plan

- [ ] Fresh install of 8.0.0 → upgrade to current master succeeds
- [ ] Tenant with `declne_to_specfy` in `patient_data.race` / `language`
/ `ethnicity` is migrated to `decline_to_specify`
- [ ] Tenant with no matching rows skips all three `#IfRow` blocks
…ad (openemr#11888)

## Summary

Loading `interface/main/main_screen.php` unconditionally regenerated the
per-session CSRF private key, silently invalidating CSRF tokens already
embedded in pages and iframes rendered before that load. Long-lived
widgets that bake their token in at render time and poll on a fixed
interval — most visibly
`interface/main/dated_reminders/dated_reminders.php` (60s poll) — then
logged `OpenEMR CSRF token authentication error` on every poll following
any in-app reload of the top frame.

The rotation looks like it was meant only for the new-login path. This
PR moves `CsrfUtils::setupCsrfKey($session)` inside the new-login branch
so the key is generated once at login and not rotated on subsequent
top-frame loads. The `else` branch is reached only after a successful
CSRF check, which already requires an existing private key, so no key
setup is needed there.

Fixes openemr#11865.

## Test plan

- [ ] Log in, capture the CSRF token rendered into
`dated_reminders.php`, reload `main_screen.php`, then re-POST the same
token to `dated_reminders.php` — should now return 200 instead of 403.
- [ ] Confirm fresh login still works (the new-login branch still runs
`setupCsrfKey`).
- [ ] Confirm `OpenEMR CSRF token authentication error` log entries no
longer appear at the 60s polling cadence after navigating between top
frames.
… string functions (openemr#11884)

Closes openemr#11880.

Convert `preg_match` calls that only match a literal prefix or suffix to
`str_starts_with()` / `str_ends_with()`, or `pathinfo()` for
file-extension checks. Skip cases that use capture groups, character
classes, or alternation that can't be expressed cleanly without a regex.

In `SQLUpgradeService::clickOptionsMigrate()`, guard against `fgets()`
returning `false` explicitly rather than letting it coerce silently into
`preg_match`. The matching baseline ignore is dropped.

## Test plan

- [x] `composer phpcs`
- [x] `composer phpstan` (baseline shrinks by one entry)
- [x] `composer php-syntax-check`
- [ ] CI green
…emr#11887)

Drains 137 entries from the `variable.undefined.php` PHPStan baseline
(openemr#11792, Phase 5).

Scope expanded beyond the original `interface/batchcom` to include:

- `interface/batchcom`
- `interface/therapy_groups`
- `interface/usergroup`
- `interface/super`
- `interface/orders`

## Fix patterns applied

- Hoist init of array accumulators / scalars before conditional blocks
- Use `?? ''` / `??=` defaults at use sites (template-style includes)
- Replace legacy globals (`$srcdir`, `$webroot`, `$webserver_root`,
`$rootdir`, `$OE_SITE_DIR`) with `OEGlobalsBag::getInstance()` typed
getters
- Replace stale `$pid` reads with
`SessionWrapperFactory::getInstance()->getActiveSession()->get('pid')`
- File-level `@var` annotations for globals provided by required files
(gacl/admin precedent openemr#11826)
- Convert `for ($i = 0, $j = $k; ...; ...)` comma-init loops to explicit
`while` (PHPStan can't follow comma expressions in for-init)

## Real bug fix

`interface/super/layout_service_codes.php` was calling `fclose($eres)`
on the wrong handle. Should be `fclose($fhcsv)`.

## Follow-up

Filed openemr#11892 to track replacing the manual loop-counter pattern (`$i =
0; while (...) { ...; ++$i; }`) with `foreach` indices and
`\Generator`s.
…ker/development-insane in the openemr-images group across 1 directory (openemr#11899)
…penemr#11895)

## Summary

Drain 341 entries from `.phpstan/baseline/variable.undefined.php` by
initializing previously-undefined variables across `interface/billing/`.
Where the offending variable was a legacy global (`$srcdir`, `$webroot`,
`$web_root`, `$webserver_root`) or a `$GLOBALS` lookup, replace it with
the typed `OEGlobalsBag` accessor (`getSrcDir()`, `getWebRoot()`,
`getString()`, `getBoolean()`).

Cap in `.phpstan/fatal-baseline-caps.php` lowered from 2927 → 2586 to
match.

Refs openemr#11792.

## Files touched

`interface/billing/`:
billing_report, billing_tracker, edi_270, edi_271, edih_main, edih_view,
edit_payment, era_payments, get_claim_file, indigent_patients_report,
new_payment, payment_master.inc, payment_pat_sel.inc,
print_billing_report, print_daysheet_report_num1/2/3, search_payments,
sl_eob_invoice, sl_eob_search, sl_receipts_report, ub04_dispose,
ub04_form, ub04_helpers.

## Test plan

- [x] `composer phpstan-baseline` regenerates with no new entries
outside `variable.undefined.php` reductions
- [x] `composer phpstan` passes (pre-commit)
- [x] `composer rector-check` clean
- [x] `composer phpcs` clean
- [ ] CI green
…ules, reports, services (openemr#11903)

Continues openemr#11792.

## Summary

Drives `variable.undefined` to **zero** in the four target directories
and lowers the cap from **3064 → 2261** (-803 entries):

| Directory | Before | After |
|---|---|---|
| `library/edihistory` | 101 | **0** |
| `interface/modules` | 95 | **0** |
| `interface/reports` | 83 | **0** |
| `src/Services` | 46 | **0** |

## Patterns applied

- Hoist initialization before conditional/loop assignment
- Replace ad-hoc globals with `OEGlobalsBag` typed getters (added
`getIncludeRoot()` helper)
- Replace direct `$_SESSION` access with `SessionWrapperFactory`
- Add file-level/standalone `@var` docblocks for include-injected vars
- Convert while-not-comma-for patterns to explicit init
- Delete dead `test_edih_835_accounting.php` (~120 entries removed;
function had no callers and contained syntactically broken trailing
code)

## Real bugs caught while draining

- `text($invnumber)` → `text($irnumber)` in
`receipts_by_method_report.php`
- Empty `$ids` left `$p` undefined in `PrescriptionTemplates` Controller
- `xl()` return discarded in faxsms `NotificationEventListener`
- `$value['reaction_text']` wrong scope in `CarecoordinationTable`
- `$combination` undefined in `EncounterccdadispatchController`
- `$status` undefined return path in
`InstallerController::InstallModule()`
- Several `edihistory` variable typos (`$rtp`/`$ft`, `$tp`/`$ft`,
`$file_type`/`$from_type`, `$err .=` on first use)

## Cleanups bundled with the drain

- `OEGlobalsBag::getIncludeRoot()` typed accessor (replaces 7 callsites
of `getString('include_root')`)
- Collapse 4-branch if/elseif over claim filetypes in
`edih_csv_data.php` into a single foreach using `http_build_query` and
`sprintf`
- `match` expression with destructuring in
`DecisionSupportInterventionService::getEmptyService`
- Replace `intval($_POST[...] ?? 0)` with `filter_input(...,
FILTER_VALIDATE_INT) ?: 0` in EHI exporter
- Type guard (`is_string` / `is_int`) instead of cast in
`EncounterccdadispatchTable::getDetails`
- `http_build_query` + `js_escape` for the redirect URL in weno
`indexrx.php`
- Replace `empty()` with explicit `=== ''` / `=== null` comparisons
where touched
- Delete commented-out and vestigial `?> <?php` blocks
- Fix bootstrap docblocks:
`Symfony\Contracts\EventDispatcher\EventDispatcherInterface` →
`Symfony\Component\EventDispatcher\EventDispatcherInterface` (matches
what `ModulesApplication` actually injects; drops 3 `argument.type`
baseline entries)

## Net baseline impact

Across all touched identifiers, hundreds of additional baseline entries
dropped beyond the variable.undefined target. A small number of
identifiers picked up entries when initialization changes widened
downstream array element types:

- `nullCoalesce.variable.php` +1 error
- `offsetAccess.invalidOffset.php` +21 errors (5 in
`interface/reports/appt_encounter_report.php`, 17 in
`library/edihistory/edih_csv_parse.php`)
- `offsetAssign.dimType.php` +1 entry / +17 errors in
`library/edihistory/edih_csv_parse.php`

These are downstream of the hoisted-init pattern and can be cleaned up
in a follow-up.

## Test plan

- [x] `composer phpstan-baseline` clean (cap = 2261, matches actual
count exactly per FatalBaselineCapsIsolatedTest)
- [x] `composer phpcs` clean
- [x] Pre-commit hooks pass (phpstan, phpcs, rector, codespell,
require-checker)
#### Short description of what this changes or resolves:
Fixes some of the setup process used during the Inferno suite, which
should upgrade some of its tests from "crashing, errors suppressed" to
"failing, errors suppressed".

On master, CI shows this:
```
Tests: 40, Assertions: 7, Errors: 34, Failures: 6, PHPUnit Warnings: 1, Warnings: 1.
```

This improves the situation significantly:

```
Tests: 41, Assertions: 965, Errors: 1, Failures: 3, PHPUnit Warnings: 1, Skipped: 1.
```

Extracted from openemr#11805

#### Changes proposed in this pull request:

- Adjust the tear-down process to not stop the containers (GHA will do
this automatically), so we can dump the logs to aid in debgging; removes
some steps that GHA will do automatically but just slow things down for
us
- Dump the logs to aid in debugging
- Run the SQL upgrade script, since apparently the data fixture is from
7.0.3
- Fix redis permissions _before_ redis is needed
- Update a timer on an expired password so it doesn't cause failures
- ~Fixes a place in the test API client to actually throw the error that
it constructs, rather than fail silently~ removes some dead code in test
setup that was a red herring in earlier work

#### Was an AI assistant used? Yes / No
Yes, Claude did most of the work on the original PR

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…1882)

## Summary

The eRx branch in `interface/patient_file/summary/demographics.php` sets
`btnLink` but never sets `linkMethod`, so `card_base.html.twig` renders
a dead `href=\"#\"` instead of linking to `eRx.php?page=compose`. The
non-eRx branch at the same call site sets both fields. Add `linkMethod =
'html'` to match.

This is a small one-line fix to the Add (+) button on the Prescription
History card when Ensora eRx is enabled (`erx_enable = 1`).

## Test plan

- [ ] Enable Ensora eRx (`erx_enable = 1`) in Admin → Globals →
Connectors
- [ ] Open a patient's demographics summary
- [ ] Verify the + button on the Prescription History card navigates to
the eRx compose page (`eRx.php?page=compose`)
- [ ] Disable eRx and verify the non-eRx branch still works (Edit button
via `editScripts(...)`)

## Notes

- Affects `master` and `rel-810` (verified `rel-810` has the same code
path at `demographics.php:1219-1227`).
- Discovered while preparing an OpenCoreEMR fork upgrade; fix originally
landed in our internal branch as openCoreEMR/openemr-internal#243.
Monica Peters and others added 28 commits May 3, 2026 11:48
…OpenERM branding on the Medical Record Dashboard
…ocs. todo: improve LangFuse ingestion metadata and env wiring
Fix cache mount id format with cacheKey prefix
Remove unsupported BuildKit cache mounts for Railpack compatibility
# Conflicts:
#	docker/development-easy/docker-compose.yml
…IR proxy

- Synced prd2_af_modernized with upstream master (docker-compose conflict: keep optional .env passthrough + upstream image pin)

- Add frontend/ App Router app: Auth.js OpenEMR OIDC, SSRF-bounded /api/fhir proxy, clinical cards + lab results, Playwright smoke

- Add Documentation/PATIENT_DASHBOARD_MIGRATION.md; update AUDIT implementation roster; gitignore allow Documentation canon + frontend artifacts

Assisted-by: Cursor Agent
@monigarr monigarr closed this May 8, 2026

# Flex is Alpine-based; root required for package installs (matches flex runtime expectations).
# hadolint ignore=DL3002
USER root
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants