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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to IDBridge are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions use
a calendar scheme `YY.M.D.build` (see [CONTRIBUTING.md](CONTRIBUTING.md#versioning--releases)).

## [26.9.9.0] - 2026-09-09

### Added
- **`Invoke-PluginPostRunBeacon` — a Beacon heartbeat PostRun plugin template.** POSTs one
`automation` envelope to SC Networks' Beacon dashboard after every run (failed and
ReadOnly runs included): `result` Success / Warning (a write did not stick) / Failed (the
run threw), a one-line message built from the counts, and the counts, flags, duration and
module version — no per-user data. Beacon's staleness model turns a nightly sync that
stops reporting amber on its own. Placeholders for the ingest URL and the district's
Beacon site id (throws until edited); the site key lives in the vault as
`ApiKey-Beacon`. Fire-and-forget like the webhook template: a send failure or a
rejection logs a `Warn` and never affects the run. The envelope builder is a pure helper
in the same file with tests under `tests\Templates\Plugins`. The config template gains
its descriptor (disabled) and moves to `TemplateVersion: 2`.

## [26.9.1.3] - 2026-09-01

### Changed
Expand Down
3 changes: 2 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ unrecognized `Tier` value fails safe to `Off`. `-DisableTelemetry` silences a si
Array of plugin descriptors. `Source`/`Override` entries are executed in order by
`Invoke-SourcePlugins` at the start of the run; `PostRun` entries by `Invoke-PostRunPlugins`
at the end of the run (in the `finally` block, after telemetry — they fire on failed and
ReadOnly runs too). The shipped config template lists all eight descriptors with
ReadOnly runs too). The shipped config template lists all nine descriptors with
`Enabled = $false`; a configured site looks like:

```powershell
Expand All @@ -124,6 +124,7 @@ ReadOnly runs too). The shipped config template lists all eight descriptors with
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunWebhook' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunExport' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunOrphanReport' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunBeacon' }
```

| Key | Effect |
Expand Down
5 changes: 3 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,12 @@ Flip one brake at a time, with a read-only-style review (`-ReadOnly` or `-Previe
OneRoster plugins in
[plugins.md](plugins.md#invoke-pluginskywardsmsstudents--source-disabled-in-config) are
worked examples).
- **Run reports, CSV exports, alerting:** the four PostRun templates consume each run's
- **Run reports, CSV exports, alerting:** the five PostRun templates consume each run's
results — `Invoke-PluginPostRunReport` (JSON run summary), `Invoke-PluginPostRunExport`
(user-list CSVs), and `Invoke-PluginPostRunOrphanReport` (enabled accounts no source
feeds anymore) work as-is once their descriptors are enabled;
`Invoke-PluginPostRunWebhook` POSTs a compact summary to your own endpoint.
`Invoke-PluginPostRunWebhook` POSTs a compact summary to your own endpoint, and
`Invoke-PluginPostRunBeacon` sends a heartbeat to SC Networks' Beacon dashboard.
See [plugins.md](plugins.md#the-postrun-contract-invoke-postrunplugins).
- **Run-history dashboard:** `Telemetry.Tier = 'Enhanced'`, then claim your SiteID
(`Get-IDBridgeSiteID`) at [IDBridge Pulse](https://pulse.scnlabs.net).
17 changes: 17 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,23 @@ throws until the placeholder URL is edited. Fire-and-forget: 10 s timeout, send
a `Warn` and never affect the run. The starting point for shipping your own telemetry or
alerting ("tell me when the run breaks").

### `Invoke-PluginPostRunBeacon` — PostRun *(disabled in config)*
File: `C:\IDBridge\Plugins\Invoke-PluginPostRunBeacon.ps1`. POSTs one heartbeat envelope to
[Beacon](https://github.com/SCNetworksLLC/Beacon), SC Networks' status dashboard, after every
run — failed and ReadOnly runs included: `sourceType` `automation`, `sourceId`
`idbridge-sync`, and a `data` block of `result` (`Success`; `Warning` when a write did not
stick; `Failed` when the run threw), a one-line `message` built from the counts
("412 managed: 3 created, 5 updated, 1 deactivated, 2 group changes, 0 write failures, 47s",
prefixed `ReadOnly:` or `test run:` in those modes), and the counts, flags, duration and
module version themselves. No per-user data. Beacon's staleness model does the rest: an
automation source is expected every ~12 hours, so a nightly sync that stops reporting goes
amber on its own and a `Failed` result goes red at once. Edit the ingest URL and the
district's Beacon site id in the file (it throws until you do), store the site key Beacon
minted for this automation with `Set-IDBridgeSecret -Name 'ApiKey-Beacon'`, then enable the
descriptor. Fire-and-forget: 30 s timeout, a rejection or a send failure logs a `Warn` and
never affects the run. The envelope builder (`ConvertTo-BeaconHeartbeat`, a helper in the
same file) is pure and tested under `tests\Templates\Plugins`.

### `Invoke-PluginPostRunExport` — PostRun *(works as-is)*
File: `C:\IDBridge\Plugins\Invoke-PluginPostRunExport.ps1`. Writes the user list CSVs
defined in its `$exportFiles` map (file name → the `PersonTypeID`s it carries; one file
Expand Down
2 changes: 1 addition & 1 deletion src/IDBridge/IDBridge.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'IDBridge.psm1'
ModuleVersion = '26.9.1.3'
ModuleVersion = '26.9.9.0'
GUID = 'a0a0c664-888e-44e2-9c12-fc8647a520c0'
Author = 'Sam Cattanach'
CompanyName = 'SC Networks LLC'
Expand Down
3 changes: 2 additions & 1 deletion src/IDBridge/Templates/Config/IDBridgeConfig.psd1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# IDBridgeConfig.psd1 — generated by Install-IDBridge
# TemplateVersion: 1
# TemplateVersion: 2
# Every feature is disabled. Replace the placeholder values, then enable features one at a time.

@{
Expand Down Expand Up @@ -93,5 +93,6 @@
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunWebhook' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunExport' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunOrphanReport' }
@{ Enabled = $false; Type = "PostRun"; Function = 'Invoke-PluginPostRunBeacon' }
)
}
115 changes: 115 additions & 0 deletions src/IDBridge/Templates/Plugins/Invoke-PluginPostRunBeacon.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Beacon Heartbeat Plugin — IDBridge PostRun plugin template
# TemplateVersion: 1
<#
Shipped with the IDBridge module and copied to <RootPath>\Plugins by Install-IDBridge.
Set the Beacon ingest URL and the site id below, store the automation's Beacon site key
in the vault (Set-IDBridgeSecret -Name 'ApiKey-Beacon'), then enable the plugin in
IDBridgeConfig.psd1 — it throws until the placeholders are edited.

POSTs one heartbeat to Beacon (SC Networks' status dashboard) after every run, failed and
ReadOnly runs included: an envelope of sourceType 'automation' whose data carries result
(Success / Warning when a write failed / Failed when the run threw), a one-line message
built from the counts ("412 managed: 3 created, 5 updated, 1 deactivated, 2 group changes,
0 write failures, 47s"), and the counts and flags themselves. Beacon's staleness model does
the rest: an automation source is expected every ~12 hours, so a nightly run that stops
reporting goes amber on its own and a Failed result goes red at once. Counts and flags
only, no per-user data.

The send is fire-and-forget: short timeout, failures are logged as a Warn and never affect
the run. The envelope is built by ConvertTo-BeaconHeartbeat below, a pure helper the tests
under tests\Templates\Plugins exercise.
#>


function Invoke-PluginPostRunBeacon {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[pscustomobject]$RunResult
)

$ingestUrl = "https://YOUR-BEACON/api/ingest" # e.g. https://beacon.scnlabs.net/api/ingest
$siteId = "YOUR-SITE-ID" # the district's Beacon site id (Beacon Setup → Sites)
$sourceId = "idbridge-sync" # how this install shows on the dashboard; one per install

if ($ingestUrl -eq "https://YOUR-BEACON/api/ingest" -or $siteId -eq "YOUR-SITE-ID") {
Throw "Invoke-PluginPostRunBeacon: this plugin template still has placeholder values. Edit $($PSCommandPath) for your district before enabling it."
}

$envelope = ConvertTo-BeaconHeartbeat -RunResult $RunResult -SiteId $siteId -SourceId $sourceId

try {
$apiKey = Get-IDBridgeSecret -Name 'ApiKey-Beacon' -AsPlainText
$response = Invoke-RestMethod -Uri $ingestUrl -Method Post -Headers @{ 'x-api-key' = $apiKey } `
-Body ($envelope | ConvertTo-Json -Depth 5 -Compress) -ContentType 'application/json' -TimeoutSec 30
if ($response.rejected -gt 0) {
Write-Log -Message "Plugin: Invoke-PluginPostRunBeacon: Beacon rejected the heartbeat (run unaffected): $($response.errors | ConvertTo-Json -Compress)" -Level Warn
}
else {
Write-Log -Message "Plugin: Invoke-PluginPostRunBeacon sent a $($envelope.data.result) heartbeat: $($envelope.data.message)" -Level Trace
}
}
catch {
Write-Log -Message "Plugin: Invoke-PluginPostRunBeacon send failed (run unaffected): $($_.Exception.GetType().Name)" -Level Warn
}
}


# The heartbeat envelope Beacon ingests, built from the RunResult alone. Pure: no config,
# no vault, no network — which is what makes it testable without a run.
function ConvertTo-BeaconHeartbeat {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[pscustomobject]$RunResult,

[Parameter(Mandatory = $true)]
[string]$SiteId,

[Parameter(Mandatory = $true)]
[string]$SourceId
)

$counts = $RunResult.Counts
$writeFailures = [int]$counts.Failed

# Failed when the run threw (a threshold abort, a startup failure, an unhandled write
# error); Warning when the run finished but a write did not stick; Success otherwise —
# a ReadOnly run that computed cleanly is a Success, its mode named in the message.
if (-not $RunResult.Success) {
$result = 'Failed'
$error = $RunResult.RunError
$reason = if ($error) { "$($error.Exception.GetType().Name): $($error.Exception.Message)" } else { 'unknown error' }
if ($reason.Length -gt 160) { $reason = $reason.Substring(0, 157) + '...' }
$message = "run failed after $($RunResult.DurationSeconds)s -- $reason"
}
else {
$result = if ($writeFailures -gt 0) { 'Warning' } else { 'Success' }
$mode = if ($RunResult.ReadOnly) { 'ReadOnly: ' } elseif ($RunResult.TestRun) { 'test run: ' } else { '' }
$message = "$mode$($counts.Managed) managed: $($counts.Create) created, $($counts.Update) updated, " +
"$($counts.Deactivate) deactivated, $([int]$counts.GroupAdd + [int]$counts.GroupRemove) group changes, " +
"$writeFailures write failures, $($RunResult.DurationSeconds)s"
}

return [ordered]@{
siteId = $SiteId
sourceType = 'automation'
sourceId = $SourceId
collectedAt = $RunResult.RunEnd.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
data = [ordered]@{
result = $result
message = $message
moduleVersion = $RunResult.ModuleVersion
durationSeconds = $RunResult.DurationSeconds
readOnly = [bool]$RunResult.ReadOnly
testRun = [bool]$RunResult.TestRun
managed = [int]$counts.Managed
created = [int]$counts.Create
updated = [int]$counts.Update
deactivated = [int]$counts.Deactivate
groupAdds = [int]$counts.GroupAdd
groupRemoves = [int]$counts.GroupRemove
writeFailures = $writeFailures
}
}
}
Loading
Loading