First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
.npmrc
|
||||||
|
dist-cjs/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
stages: [test]
|
||||||
|
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
image: node:22-alpine
|
||||||
|
script: [npm ci, npm run build, npm test]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0 - 2026-09-28
|
||||||
|
|
||||||
|
First public release under MIT.
|
||||||
|
|
||||||
|
- `unify()` and `formatPane()` roll signals from different sources into one
|
||||||
|
report in the `@extant2000/evidence-record` format.
|
||||||
|
- Readings older than the freshness window, or with no timestamp, are not
|
||||||
|
assessed, never green. `now` is injected.
|
||||||
|
- `ambiguousAttribution()` names subjects a source files under one shared
|
||||||
|
identity, and their per-subject readings are not assessed.
|
||||||
|
- Expected subjects with no signal, uncomputable values and empty boards are
|
||||||
|
reported as not assessed.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Issues and merge requests are welcome at
|
||||||
|
https://gitlab.com/extant2000/one-pane.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
- **A test that cannot fail proves nothing.** If you fix a bug, add a test
|
||||||
|
that fails without your fix, and check that it does fail before you submit.
|
||||||
|
- Measure, do not assume. Two modules with the same line count can be
|
||||||
|
different programs.
|
||||||
|
- Keep dependencies minimal. Every new dependency needs a reason.
|
||||||
|
- Explain why in comments, not what.
|
||||||
|
|
||||||
|
## Before you open a merge request
|
||||||
|
|
||||||
|
Run both of these and make sure they pass:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
npm test
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Extant 2000 LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# OnePane
|
||||||
|
|
||||||
|
Rolls health signals from different tools and stacks into one report, and
|
||||||
|
refuses to show a signal as healthy when it cannot be trusted.
|
||||||
|
|
||||||
|
## What it does and why
|
||||||
|
|
||||||
|
Collecting status from an uptime checker, an error tracker, CI and each
|
||||||
|
service's own endpoints into one board is mostly plumbing. The part that is
|
||||||
|
not plumbing is knowing when a green panel is lying. It lies in two specific
|
||||||
|
ways, and OnePane is built against both.
|
||||||
|
|
||||||
|
### Silence shown as health
|
||||||
|
|
||||||
|
An error board shows zero events for every product. They are not healthy:
|
||||||
|
the ingestion quota ran out and nothing is being recorded. Every panel is
|
||||||
|
green because no signal arrived, and that is the one state a dashboard must
|
||||||
|
never draw the same as "fine". So a reading older than the freshness window
|
||||||
|
(15 minutes by default) is reported as not assessed, with the note that the
|
||||||
|
panel is showing its last reading, not the current state. A reading with no
|
||||||
|
timestamp is treated the same way, because it cannot be told apart from a
|
||||||
|
stale one.
|
||||||
|
|
||||||
|
### Shared identity taken as per-subject evidence
|
||||||
|
|
||||||
|
Several products report through one error-tracking project. A per-product
|
||||||
|
search returns nothing and reads as "zero errors in this product", when it
|
||||||
|
means "this product cannot be separated in the data". Absence of attributed
|
||||||
|
events is not absence of events. When two subjects from the same source
|
||||||
|
share an `attributedTo` value, `ambiguousAttribution()` names them ("errors
|
||||||
|
cannot separate api, shop"), and every per-subject reading from that source
|
||||||
|
is reported as not assessed. A named list is something you can act on;
|
||||||
|
"attribution ambiguous" is not.
|
||||||
|
|
||||||
|
The rest of the rules:
|
||||||
|
|
||||||
|
- a subject in `expectedSubjects` with no signal at all is reported, since it
|
||||||
|
is green only because nothing was ever wired to it;
|
||||||
|
- a `null` value with no health state is not assessed, never coerced to zero;
|
||||||
|
- `down` fails at critical severity and `degraded` at high;
|
||||||
|
- an empty board is not assessed, never healthy.
|
||||||
|
|
||||||
|
The library is pure: no HTTP and no clock. `now` is a parameter, because a
|
||||||
|
freshness check that reads the clock cannot be tested and its output cannot
|
||||||
|
be reproduced. Reports use the `@extant2000/evidence-record` format.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { unify, formatPane, ambiguousAttribution, type Signal } from '@extant2000/one-pane'
|
||||||
|
|
||||||
|
const signals: Signal[] = [
|
||||||
|
{ source: 'uptime', subject: 'shop', metric: 'availability', value: 100, health: 'up',
|
||||||
|
observedAt: '2026-09-28T11:58:00Z', where: 'https://status.example.com' },
|
||||||
|
{ source: 'errors', subject: 'shop', metric: 'errors', value: 0, health: 'up',
|
||||||
|
observedAt: '2026-09-28T11:59:00Z', attributedTo: 'shared-project', where: 'error tracker search' },
|
||||||
|
{ source: 'errors', subject: 'api', metric: 'errors', value: 0, health: 'up',
|
||||||
|
observedAt: '2026-09-28T11:59:00Z', attributedTo: 'shared-project', where: 'error tracker search' },
|
||||||
|
{ source: 'ci', subject: 'billing', metric: 'last-deploy', value: 1, health: 'up',
|
||||||
|
observedAt: '2026-09-26T08:00:00Z', where: 'ci pipeline' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
now: '2026-09-28T12:00:00Z', // injected, never read from the clock
|
||||||
|
maxAgeSeconds: 900,
|
||||||
|
expectedSubjects: ['shop', 'api', 'billing', 'docs'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = unify(signals, opts)
|
||||||
|
console.log(formatPane(signals, opts))
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example only the shop availability reading passes. The two error
|
||||||
|
counts of zero are not assessed because both products share one project, the
|
||||||
|
billing reading is two days old, and `docs` has no signal at all.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install @extant2000/one-pane
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT. See [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||||
|
Do not open a public issue for a security problem.
|
||||||
|
|
||||||
|
We aim to acknowledge every report within 5 business days.
|
||||||
Generated
+1503
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "@extant2000/one-pane",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": false,
|
||||||
|
"description": "Unifies health signals across stacks and refuses to show stale, unstamped or unattributable signals as healthy.",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist-cjs/index.js",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"dist-cjs",
|
||||||
|
"src",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitlab.com/extant2000/one-pane.git"
|
||||||
|
},
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist-cjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
|
||||||
|
"test": "vitest run",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vitest": "^2.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@extant2000/evidence-record": "^0.1.2"
|
||||||
|
},
|
||||||
|
"author": "Extant 2000 LLC",
|
||||||
|
"homepage": "https://gitlab.com/extant2000/one-pane",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitlab.com/extant2000/one-pane/-/issues"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,133 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { overall, validateReport } from '@extant2000/evidence-record'
|
||||||
|
import { ambiguousAttribution, formatPane, unify, type Signal } from '../src/index.js'
|
||||||
|
|
||||||
|
const NOW = '2026-08-01T12:00:00Z'
|
||||||
|
const fresh = '2026-08-01T11:58:00Z'
|
||||||
|
const old = '2026-08-01T09:00:00Z'
|
||||||
|
|
||||||
|
const sig = (p: Partial<Signal> = {}): Signal => ({
|
||||||
|
source: 'gatus',
|
||||||
|
subject: 'crm',
|
||||||
|
metric: 'availability',
|
||||||
|
value: 100,
|
||||||
|
health: 'up',
|
||||||
|
observedAt: fresh,
|
||||||
|
where: 'https://status.example.com',
|
||||||
|
...p,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('silence is not health', () => {
|
||||||
|
it('a stale panel is NOT ASSESSED, not green', () => {
|
||||||
|
// An error board can show zero events because ingestion stopped, not
|
||||||
|
// because nothing went wrong.
|
||||||
|
const r = unify([sig({ observedAt: old })], { now: NOW })
|
||||||
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
||||||
|
expect(overall(r)).toBe('not-assessed')
|
||||||
|
const text = formatPane([sig({ observedAt: old })], { now: NOW })
|
||||||
|
expect(text).toContain('Silence is not health')
|
||||||
|
expect(text).toContain('showing its last reading')
|
||||||
|
expect(text).not.toContain('✓')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes a reading inside the freshness window', () => {
|
||||||
|
expect(overall(unify([sig()], { now: NOW }))).toBe('pass')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('honours a custom freshness window', () => {
|
||||||
|
// 2 minutes old, with a 60-second window.
|
||||||
|
expect(overall(unify([sig()], { now: NOW, maxAgeSeconds: 60 }))).toBe('not-assessed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats an unstamped reading as indistinguishable from a stale one', () => {
|
||||||
|
const r = unify([sig({ observedAt: undefined })], { now: NOW })
|
||||||
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
||||||
|
expect(r.findings[0]!.detail).toContain('may be current; nothing here establishes that')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports an expected subject with no signal at all', () => {
|
||||||
|
const r = unify([sig({ subject: 'crm' })], { now: NOW, expectedSubjects: ['crm', 'hr', 'desk'] })
|
||||||
|
const missing = r.findings.filter(f => f.id.endsWith('/missing'))
|
||||||
|
expect(missing).toHaveLength(2)
|
||||||
|
expect(missing[0]!.detail).toContain('green because nothing was ever wired to it')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shared identity is not per-subject evidence', () => {
|
||||||
|
// Several products reporting through ONE error-tracking project. A
|
||||||
|
// per-product search returning nothing means "not separable", not "no errors".
|
||||||
|
const shared = [
|
||||||
|
sig({ source: 'sentry', subject: 'crm', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
||||||
|
sig({ source: 'sentry', subject: 'hr', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
||||||
|
sig({ source: 'sentry', subject: 'desk', metric: 'errors', value: 0, attributedTo: 'shared-project' }),
|
||||||
|
]
|
||||||
|
|
||||||
|
it('names the entangled subjects rather than saying "ambiguous"', () => {
|
||||||
|
const groups = ambiguousAttribution(shared)
|
||||||
|
expect(groups).toHaveLength(1)
|
||||||
|
expect(groups[0]!.subjects).toEqual(['crm', 'desk', 'hr'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refuses to read a shared-identity zero as a per-subject pass', () => {
|
||||||
|
const r = unify(shared, { now: NOW })
|
||||||
|
expect(overall(r)).toBe('not-assessed')
|
||||||
|
for (const f of r.findings.filter(f => f.id.startsWith('crm/'))) {
|
||||||
|
expect(f.determination).toBe('not-assessed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('says what a search returning nothing actually means', () => {
|
||||||
|
const text = formatPane(shared, { now: NOW })
|
||||||
|
expect(text).toContain('absence of attributed events is not absence of events')
|
||||||
|
expect(text).toContain('crm, desk, hr')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves a uniquely-attributed signal alone', () => {
|
||||||
|
const distinct = [
|
||||||
|
sig({ source: 'sentry', subject: 'crm', attributedTo: 'crm-prod' }),
|
||||||
|
sig({ source: 'sentry', subject: 'hr', attributedTo: 'hr-prod' }),
|
||||||
|
]
|
||||||
|
expect(ambiguousAttribution(distinct)).toEqual([])
|
||||||
|
expect(overall(unify(distinct, { now: NOW }))).toBe('pass')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not entangle the same identity across different sources', () => {
|
||||||
|
// gatus/"prod" and sentry/"prod" are unrelated namespaces.
|
||||||
|
const r = ambiguousAttribution([
|
||||||
|
sig({ source: 'gatus', subject: 'crm', attributedTo: 'prod' }),
|
||||||
|
sig({ source: 'sentry', subject: 'hr', attributedTo: 'prod' }),
|
||||||
|
])
|
||||||
|
expect(r).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('health states', () => {
|
||||||
|
it('reports down as critical and degraded as high', () => {
|
||||||
|
expect(unify([sig({ health: 'down' })], { now: NOW }).findings[0]!.severity).toBe('critical')
|
||||||
|
expect(unify([sig({ health: 'degraded' })], { now: NOW }).findings[0]!.severity).toBe('high')
|
||||||
|
expect(overall(unify([sig({ health: 'down' })], { now: NOW }))).toBe('fail')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports an uncomputable value as not-assessed, not zero', () => {
|
||||||
|
const r = unify([sig({ value: null, health: undefined })], { now: NOW })
|
||||||
|
expect(r.findings[0]!.determination).toBe('not-assessed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('carries the sample count into the citation for a null value', () => {
|
||||||
|
const text = formatPane([sig({ value: null, health: undefined })], { now: NOW, evidence: 'inline' })
|
||||||
|
expect(text).toContain('not computable')
|
||||||
|
expect(text).toContain('0 samples')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('conformance with the evidence-record standard', () => {
|
||||||
|
it('an empty board is not-assessed, never healthy', () => {
|
||||||
|
const r = unify([], { now: NOW })
|
||||||
|
expect(overall(r)).toBe('not-assessed')
|
||||||
|
expect(formatPane([], { now: NOW })).toContain('An empty board is not a healthy one')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('every conclusion carries a citation', () => {
|
||||||
|
expect(validateReport(unify([sig(), sig({ subject: 'hr', health: 'down' })], { now: NOW }))).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"outDir": "dist-cjs",
|
||||||
|
"declaration": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
|
||||||
|
"declaration": true, "outDir": "dist", "rootDir": "src",
|
||||||
|
"strict": true, "skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user