apiforgejs 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "apiforgejs",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "API observability & intelligence SDK for Express.js — local-first, privacy-first",
5
5
  "main": "src/index.js",
6
6
  "keywords": [
7
- "api", "observability", "monitoring", "express", "metrics",
8
- "performance", "analytics", "middleware", "latency", "local-first"
7
+ "api",
8
+ "observability",
9
+ "monitoring",
10
+ "express",
11
+ "metrics",
12
+ "performance",
13
+ "analytics",
14
+ "middleware",
15
+ "latency",
16
+ "local-first"
9
17
  ],
10
18
  "author": "APIForge",
11
19
  "license": "MIT",
package/src/database.js CHANGED
@@ -229,6 +229,19 @@ class ApiForgeDatabase {
229
229
  this._begin.run();
230
230
  try {
231
231
  for (const r of routes) stmt.run(r.route, r.method);
232
+ // Remove routes no longer in the router that never had traffic
233
+ if (routes.length > 0) {
234
+ const keys = routes.map(r => `${r.route}|${r.method}`);
235
+ const ph = keys.map(() => '?').join(', ');
236
+ this.db.prepare(`
237
+ DELETE FROM known_routes
238
+ WHERE route || '|' || method NOT IN (${ph})
239
+ AND NOT EXISTS (
240
+ SELECT 1 FROM api_metrics m
241
+ WHERE m.route = known_routes.route AND m.method = known_routes.method
242
+ )
243
+ `).run(...keys);
244
+ }
232
245
  this._commit.run();
233
246
  } catch (err) {
234
247
  this._rollback.run();
@@ -258,13 +271,17 @@ class ApiForgeDatabase {
258
271
 
259
272
  getReleases() {
260
273
  return this.db.prepare(`
261
- SELECT release_tag,
262
- MIN(bucket_ts) as release_ts,
263
- COUNT(DISTINCT route || '|' || method) as routes_affected
264
- FROM api_metrics
265
- WHERE release_tag IS NOT NULL AND release_tag != ''
266
- GROUP BY release_tag
267
- ORDER BY release_ts DESC
274
+ WITH release_times AS (
275
+ SELECT release_tag, MIN(bucket_ts) AS release_ts
276
+ FROM api_metrics
277
+ WHERE release_tag IS NOT NULL AND release_tag != ''
278
+ GROUP BY release_tag
279
+ )
280
+ SELECT rt.release_tag,
281
+ rt.release_ts,
282
+ (SELECT COUNT(*) FROM known_routes WHERE first_seen <= rt.release_ts + 60) AS routes_affected
283
+ FROM release_times rt
284
+ ORDER BY rt.release_ts DESC
268
285
  LIMIT 20
269
286
  `).all();
270
287
  }
package/src/ui.html CHANGED
@@ -421,6 +421,7 @@ function mapInsights(insights) {
421
421
  function mapReleases(releases) {
422
422
  return (releases || []).map(r => ({
423
423
  tag: r.release_tag,
424
+ ts: r.release_ts,
424
425
  summary: `${r.routes_affected || 0} route${r.routes_affected !== 1 ? 's' : ''} recorded`,
425
426
  age: formatAge(r.release_ts),
426
427
  by: 'local',
@@ -717,7 +718,7 @@ function StatusStackChart({ data, height = 200 }) {
717
718
  }
718
719
 
719
720
  // ─── Overview ─────────────────────────────────────────────────────────────────
720
- function Overview({ timeRange, setRoute, setParams }) {
721
+ function Overview({ timeRange, setRoute, setParams, lastUpdated }) {
721
722
  const { ENDPOINTS, RELEASES, INSIGHTS, SUMMARY } = window.AF_DATA;
722
723
  const [globalTs, setGlobalTs] = useState(null);
723
724
  const hours = TIME_HOURS[timeRange] || 24;
@@ -726,7 +727,7 @@ function Overview({ timeRange, setRoute, setParams }) {
726
727
  setGlobalTs(null);
727
728
  fetch(`/api/global-timeseries?hours=${hours}`)
728
729
  .then(r => r.json()).then(d => setGlobalTs(d)).catch(() => setGlobalTs([]));
729
- }, [hours]);
730
+ }, [hours, lastUpdated]);
730
731
 
731
732
  const chartData = globalTs ? tsBucketsToChart(globalTs, hours) : null;
732
733
  const points = Math.max(chartData?.p90?.length || 0, 2);
@@ -739,11 +740,18 @@ function Overview({ timeRange, setRoute, setParams }) {
739
740
  const globalCalls = chartData?.calls || Array(fallbackPts).fill(0);
740
741
  const xLabelsFinal = xLabels.length > 0 ? xLabels : Array.from({length:globalP90.length}, (_,i) => `${i}`);
741
742
 
742
- const releaseMarkers = (RELEASES || []).slice(0,2).map((r, i) => ({
743
- idx: Math.min(Math.floor(globalP90.length * (0.45 + i * 0.35)), globalP90.length - 1),
744
- label: r.tag,
745
- color: i === 0 ? '#b91c1c' : '#15803d',
746
- }));
743
+ const MARKER_COLORS = ['#b91c1c','#15803d','#2563eb','#b45309','#7c3aed'];
744
+ const nowTs = Date.now() / 1000;
745
+ const releaseMarkers = globalTs && globalTs.length > 0
746
+ ? [...(RELEASES || [])]
747
+ .filter(r => r.ts != null && r.ts >= nowTs - hours * 3600)
748
+ .reverse()
749
+ .map((r, i) => {
750
+ const idx = globalTs.reduce((best, b, j) =>
751
+ Math.abs(b.bucket_ts - r.ts) < Math.abs(globalTs[best].bucket_ts - r.ts) ? j : best, 0);
752
+ return { idx, label: r.tag, color: MARKER_COLORS[i % MARKER_COLORS.length] };
753
+ })
754
+ : [];
747
755
 
748
756
  const topSlow = [...ENDPOINTS].filter(e => !e.untracked && e.base_p90 > 0).sort((a,b) => b.base_p90-a.base_p90).slice(0,5);
749
757
  const topCalled = [...ENDPOINTS].sort((a,b) => b.calls24h-a.calls24h).slice(0,5);
@@ -1048,7 +1056,7 @@ function Endpoints({ setRoute, setParams }) {
1048
1056
  }
1049
1057
 
1050
1058
  // ─── Endpoint detail ──────────────────────────────────────────────────────────
1051
- function EndpointDetail({ id, timeRange, setRoute, setParams }) {
1059
+ function EndpointDetail({ id, timeRange, setRoute, setParams, lastUpdated }) {
1052
1060
  const { ENDPOINTS, INSIGHTS } = window.AF_DATA;
1053
1061
  const ep = ENDPOINTS.find(e => e.id === id) || ENDPOINTS[0];
1054
1062
  const [tab, setTab] = useState('performance');
@@ -1063,7 +1071,7 @@ function EndpointDetail({ id, timeRange, setRoute, setParams }) {
1063
1071
  setTs(null);
1064
1072
  fetch(`/api/timeseries?route=${encodeURIComponent(route)}&method=${encodeURIComponent(method)}&hours=${hours}`)
1065
1073
  .then(r => r.json()).then(d => setTs(d)).catch(() => setTs([]));
1066
- }, [id, hours]);
1074
+ }, [id, hours, lastUpdated]);
1067
1075
 
1068
1076
  if (!ep) return <div className="empty-state">Endpoint not found.</div>;
1069
1077
 
@@ -1626,9 +1634,9 @@ function App() {
1626
1634
  lastUpdated={lastUpdated} onRefresh={() => fetchData.current()}/>
1627
1635
  <div className="content">
1628
1636
  <div className="content-inner">
1629
- {route === 'overview' && <Overview timeRange={timeRange} setRoute={setRoute} setParams={setParams}/>}
1637
+ {route === 'overview' && <Overview timeRange={timeRange} setRoute={setRoute} setParams={setParams} lastUpdated={lastUpdated}/>}
1630
1638
  {route === 'endpoints' && <Endpoints setRoute={setRoute} setParams={setParams}/>}
1631
- {route === 'endpoint' && <EndpointDetail id={params.id} timeRange={timeRange} setRoute={setRoute} setParams={setParams}/>}
1639
+ {route === 'endpoint' && <EndpointDetail id={params.id} timeRange={timeRange} setRoute={setRoute} setParams={setParams} lastUpdated={lastUpdated}/>}
1632
1640
  {route === 'insights' && <Insights setRoute={setRoute} setParams={setParams}/>}
1633
1641
  {route === 'releases' && <Releases/>}
1634
1642
  {route === 'settings' && <Settings/>}