feat(hub): URL-keyed knowledge surface + CHAOSS metrics dashboard + Overview polish#50
Merged
Conversation
Adds /hub — a public-by-default surface where every URL in the open-
pulse ecosystem (GitHub, HuggingFace, Zenodo, Infoscience, ROR,
OpenAlex, ETHZ research-collection, ORCID, …) gets a dedicated page
at /hub/<host>/<path> that pulls live facts from the four stores:
* Neo4j · community graph (Repo / User / Org · CONTRIBUTES_TO /
OWNS / FORK_OF / MEMBER_OF)
* SPARQL · repository properties (schema:* and pulse:* triples
against Oxigraph)
* Qdrant · GME-extracted payloads and cross-collection backlinks
(papers, datasets, models that mention this entity)
* OpenSearch · GrimoireLab-indexed git / GitHub activity, monthly
commit sparkline, contributor cardinality
Highlights of the surface:
* Per-host URL resolvers + a generic fallback. Auth-optional via
HUB_PUBLIC_KNOWLEDGE; the hub never persists credentials.
* Skeleton + SSE streaming for the entity body; lazy panels for
backlinks, related, activity, connected GitHub repos, community.
* Navigation trail (?chain=) encoded in the URL with localStorage
cache — breadcrumbs reflect the user's path through the network,
not just the entity's ancestry.
* Hover tooltips on every related/backlink card fetch a live
/api/hub/preview/{ref} payload so visitors see what they're about
to navigate to before clicking.
* Hub home: type-ahead search (Qdrant text-match across 18 high-
value collections in parallel), Sources grid with logos +
homepage links + heatmap shading, Top-N leaderboards backed by
DuckDB-cached statistics (1 h TTL).
* Wanted list backlog (SQLite) for unknown URLs so the crawler can
pick them up later.
* EPFL RCP token fall-back for the small narration agent when no
HUB_LLM_API_KEY is configured.
* /databases console: tabs renamed and reordered (Community Graph ·
Neo4j → Repositories Properties · SPARQL → Grimoire Lab ·
OpenSearch), much-expanded example library (38 chips total), and
a syntax-highlighted snippet block (highlight.js, atom-one-dark)
that re-paints whenever the editor, language pill, engine, or
OpenSearch mode changes.
* Sidebar gains a "Hub" entry under Explore, and base.html is
restructured so the page header + breadcrumb live in a sticky
.page-chrome strip under the marquee.
Everything is keyed off the same parse_ref() normalisation, so a URL
typed in /hub home, clicked from a tile, pasted from a paper, or
linked from a Qdrant payload all land on the same canonical page.
New /chaoss surface with five "Level 0 · Must-have" metrics from the
CHAOSS catalogue, each computed live against the relevant subset of
the four open-pulse stores and rendered with the exact query text
that produced the number so visitors can audit and reproduce it.
metric category time-based? stores
────────────────────────────────────────────────────────────────
contributors Community yes (window) Neo4j · SPARQL · OS
new_contributors Community yes (window) SPARQL · OS
technical_fork Popularity no (snap) Neo4j · SPARQL
licenses_declared FAIR-quality no (snap) SPARQL
academic_impact Popularity no (snap) Qdrant (4 cols)
Surface:
* /chaoss — catalogue + repo selector. Window choices
run from 30 d to 10 y so older repos whose
activity is all pre-2024 still light up.
* /chaoss/github.com/{owner}/{repo}?window={days}
— per-repo dashboard with five skeleton cards
hydrated in parallel via the API below.
* /api/chaoss/github.com/{owner}/{repo}/{slug}
— HTML fragment per metric: headline value,
multi-store breakdown, example rows, the
methodology paragraph, and a collapsible
"How we compute this · N queries" block.
Transparency:
* Each card carries the literal Cypher / SPARQL / OpenSearch DSL
that ran, syntax-highlighted via the same highlight.js theme used
on /databases.
* Per-trace "Open in /databases" deep-link that pre-fills the editor
via the URL fragment (#engine=…&q=…&os_mode=…).
* Per-trace "copy query" button.
* Failed traces show the error verbatim instead of disappearing.
Headline policy:
* For multi-store metrics, the headline prefers the largest
non-zero value among the windowed stores, falling back to Neo4j's
all-time count. Guards against GrimoireLab returning 0 because a
repo isn't indexed yet hiding a legitimate signal from SPARQL.
Sidebar gains a "CHAOSS metrics" entry under Explore, alongside
Hub / Query.
… closure ratio, org diversity)
Doubles the dashboard to ten cards. New metrics:
project_popularity stars + forks (SPARQL) + DEPENDS_ON
dependents (Neo4j). Lists dependent repos by
name in the examples row — for sdsc-ordes/gimie
that's gimie-api / git-metadata-extractor /
alphavector / Lyn4ever29.
programming_languages schema:programmingLanguage set per repo
(presence, not byte share — byte weighting
is Phase 3 once the extractor emits it).
activity_dates monthly commit histogram from GrimoireLab,
rendered as an inline SVG sparkline so the
card shows the cadence at a glance. Headline
= total commits in window; secondary calls
out the busiest month.
closure_ratio closed / total pull requests in the window,
with merged-vs-just-closed broken out. Hits
github_*_enriched indices with pull_request
+ state + merged filters.
org_diversity distinct organisations whose members
contribute to the repo, walked through
Person → org:hasMembership → Org → schema:name.
Card template now renders ``result.series`` as a minimal inline-SVG
sparkline (no chart library) — opacity scales with each month's value
so high-traffic months read as deeper bars without a tooltip.
Headline policy is per-metric:
* project_popularity: stars when present, else dependents, else forks
* activity_dates: total commits in window
* closure_ratio: closure percentage
* org_diversity: distinct org count
* programming_languages: count of declared languages
Two small UI polishes:
* /chaoss landing reorders sections so the four "Try one" shortcuts
sit immediately under the repo selector — visitors get a
one-click path into a populated dashboard before having to read
the metric catalogue.
* The activity_dates sparkline gains hover affordance. Each <rect>
now carries a native <title> child (free accessibility + works
when JS is disabled), and the whole sparkline is wrapped in a
tiny Alpine x-data block that tracks the hovered index and floats
a small chip above the chart with "YYYY-MM · N commits". The chip
is positioned absolutely so it doesn't get clipped by the card
edge, and clears on mouseleave.
Four more cards, all on the Community / responsiveness axis:
first_response P50 hours from PR/issue open to first non-author
comment. Uses GrimoireLab's precomputed
``time_to_first_attention_without_bot`` enrichment
so the metric stays consistent across backends.
issue_resolution P50 days from issue open to close, excluding PRs
(``pull_request: false``). Reads GrimoireLab's
``time_open_days``.
self_merge Of the merged PRs in the window, how many had
the author == merge_author? Uses a Painless
script aggregation so the comparison happens in
the cluster.
burstiness CHAOSS Goh-Barabási burstiness B = (σ − μ)/(σ + μ)
on inter-arrival days between commits. Range:
−1 (perfectly periodic) → 0 (Poisson) → +1
(heavy bursts). Computed client-side from a
daily date_histogram so the query shown to the
visitor is exactly what ran.
The three github_*_enriched-backed metrics honestly report "no data"
in stacks where GrimoireLab hasn't been pointed at any issue/PR
backend yet — the query trace stays visible so future operators can
see exactly what will start working once the github_collector lands.
Burstiness runs on git_*_enriched which is already populated, so it
returns real values today (gimie: B=+0.53 over 5 years, "bursty").
Three more cards on the Community axis, all on git_*_enriched:
absence_factor Smallest N contributors whose combined commits
cover 50 % of the project's total ("bus factor").
Walks a descending terms agg in Python until the
cumulative share crosses the threshold. Regime
label: fragile (1–2), concentrated (3–5),
distributed (6+). Lists the top contributors
with their per-author share in examples.
project_demographics Population breakdown: total contributors, 'core'
(top N covering 80 %), 'recent arrivals' (first
commit in last 90 d), 'dormant' (no commit in
180 d). One query — terms agg + per-bucket
min(date) and max(date) sub-aggs.
bot_activity Share of commits whose author_name matches any
of a recognised-bot wildcard list (dependabot,
renovate, github-actions, *[bot]*, mergify,
release-please, …). Uses a filter agg with
should-OR wildcards; lists which bots showed up
in examples.
Also fixes a latent bug in earlier OpenSearch-backed metrics: GrimoireLab
indexes ``author_name`` as a keyword field directly — no ``.keyword``
sub-mapping. The wrong field name caused contributors / new_contributors
to return 0 from OpenSearch on gimie even though the index had 505
commits. Now they report 16 unique contributors with the bus factor
landing at 2 (cmdoret + rmfranken cover 52 % of work).
22 metrics now on the dashboard. Five new ones land on Community:
issues_new Issues opened in the window (pull_request:false +
created_at range).
issues_active Issues with any update in the window (updated_at).
issues_closed Issues whose state flipped to closed (state:closed
+ closed_at range).
cr_reviews PRs that received at least one non-bot review,
from GrimoireLab's num_review_comments_without_bot.
code_lines Sum of lines_added + lines_removed, with
file-change count broken out as secondary. For
sdsc-ordes/gimie over 5 y: 67,394 lines (+45,917
added / −21,477 removed) across 505 commits and
1,105 file-changes — real numbers from
git_*_enriched.
Topic axis goes from one big grid to three category sections —
Popularity / Community / FAIR-quality — with section headings,
per-section counts, and a filter pill row at the top of both
/chaoss landing and /chaoss/<repo>.
* One Alpine ``chaossFilter`` component, used on both pages, holds
the list of active category names. Clicking a pill toggles its
section's visibility; the "all" pill restores everything; an
empty active list auto-restores so the page never looks broken.
* Toggled-off pills get dimmed + strike-through via the new
``.pill.is-inactive`` style in app.css.
* Routes ship a ``groups`` list (one entry per non-empty category,
in the canonical SDSC order) and a ``categories`` list (the pill
metadata: name + css class + blurb).
Replaces the SDSC custom buckets (Popularity / Community / FAIR-quality)
with the topics CHAOSS itself uses on chaoss.community/kbtopic/. Four
topics surface in open-pulse so far:
Contributor 4 metrics contributors, new_contributors,
absence_factor, project_demographics
Software 7 metrics technical_fork, licenses_declared,
academic_impact, project_popularity,
programming_languages, bot_activity,
code_lines
Lifecycle 10 metrics activity_dates, closure_ratio,
first_response, issue_resolution,
self_merge, burstiness, issues_new,
issues_active, issues_closed, cr_reviews
Organization 1 metric org_diversity
Each topic section header carries a "CHAOSS catalogue ↗" link
pointing at chaoss.community/kbtopic/<slug>/ so visitors can browse
the official metric catalogue for that topic side-by-side with the
open-pulse implementation.
The filter pill row + Alpine chaossFilter() component stay unchanged —
they just operate on the new topic names.
The CHAOSS dashboard is meant to be transparent and reproducible:
every value on a card has to come from a query the visitor can paste
straight into /databases. RAG-mediated answers can't be re-run by hand,
so they shouldn't power a number on the page.
academic_impact was the only metric still calling the vector store
(qdrant._autocomplete_one across four scholarly collections). It now
walks a SPARQL chain instead:
?repo pulse:githubRepositoryHandle "owner/repo" .
?person pulse:hasContribution ?contrib .
?contrib pulse:contributionTo ?repo .
?article a schema:ScholarlyArticle ;
schema:author ?person .
i.e. "scholarly articles whose author has also contributed to the
repo". This is a weaker signal than "papers that cite the software",
because the current ontology emits no direct article→repo predicate
(no schema:isBasedOn / schema:codeRepository / schema:citation), but
it is fully transparent. The metric's notes call out the proxy and
flag that it will switch to the direct triple once the extractor
emits it.
Also drops the unused ``qdrant`` import from chaoss/metrics.py.
…d fix /databases deep-link
GUI polish on the CHAOSS dashboard, plus a long-standing rough edge
in the /databases console.
GUI visuals
* MetricResult gains optional ``visual`` (typed visualisation hint)
and ``headline_tone`` (severity colour for the big number).
* Three visualisation kinds wired in metric compute functions:
donut — closure_ratio, cr_reviews, self_merge, bot_activity
stacked_bar — project_demographics (core / active / recent / dormant)
rank_bars — absence_factor (top-N contributors, bar width = share)
* Severity-coloured headlines:
absence_factor → danger when 1–2 / warn when 3–5 / good when 6+
self_merge → danger when ≥ 50 % / warn when ≥ 20 % / good below
cr_reviews → good when ≥ 60 % / warn when ≥ 30 % / danger below
* Four CSS semantic tones (good / info / warn / danger) defined as
variables so light + dark themes stay coordinated.
* Topic section headers gain inline SVG icons:
Contributor — two-person silhouette
Software — stacked packages
Lifecycle — circular arrow
Organization — office building
/databases deep-link
* The "Open in /databases" button on every metric trace builds a
URL with ``#engine=…&q=…&os_mode=…``. The console was reading
only localStorage on init, so the fragment was ignored and the
user landed on whichever tab they'd opened last.
* New consumeDeepLink() on dbConsole runs after init(): parses the
fragment, switches engine, spawns a fresh tab seeded with the
decoded query, applies os_mode if present, and clears the
fragment so a page reload doesn't re-fire the link.
Verified live on sdsc-ordes/gimie:
* bus-factor headline reads "2 · fragile · one or two key people"
in red, with rank-bars showing cmdoret at 100 % bar width down to
supermaxiste at 5 %
* demographics shows a green-blue-amber-red stacked strip:
5 core · 0 active · 0 recent · 11 dormant
* bot_activity donut: 0 % filled (0 of 505 commits matched a bot
pattern) — visually unambiguous about a clean human-only repo
…bots, papers
Adds monthly (or yearly for academic_impact) trend series to metrics
that previously showed only a single number, so the visitor reads
both magnitude *and* trajectory at a glance. Six more cards now
render the inline-SVG sparkline:
contributors monthly unique authors (cardinality sub-agg per
date_histogram bucket — one OpenSearch round-trip)
issues_new monthly opened issues
issues_active monthly issues with any update
issues_closed monthly issues whose state flipped to closed
code_lines monthly churn (added + removed)
bot_activity monthly bot-authored commit count
academic_impact one bar per publication YEAR from the schema:
datePublished triples, with no-publication years
rendered as gaps so cadence is visible
Generic ``series_unit`` (defaults to "events") goes on MetricResult so
the sparkline hover chip reads the right noun:
activity_dates "commits"
contributors "contributors"
issues_* "issues"
code_lines "lines"
bot_activity "bot commits"
academic_impact "papers"
The shared ``_issues_count`` helper now bakes the monthly histogram
into its single OpenSearch round-trip — no extra calls per metric.
Three things conspired to make the section-header icons render at
viewport size:
* The CSS rule defining .chaoss-topic-icon (and .pill.is-inactive,
--chaoss-tone-*) got lost in an earlier edit churn — app.css
hadn't actually changed since the original hub commit, so the
class did nothing.
* The SVG had no width / height attributes, so without CSS the
viewport-relative default kicked in.
* The icon sits in a flex row, which gave the unsized SVG the
whole free space.
Belt-and-braces fix:
* Re-add the entire CHAOSS CSS block at the end of app.css
(.pill.is-inactive · the four --chaoss-tone-* variables for both
themes · svg.chaoss-topic-icon with !important sizing).
* Inline width="18" / height="18" attributes on the <svg> in both
landing.html and repo.html.
* Inline style="width: 18px; height: 18px; flex: 0 0 18px"
alongside, so even if a future minifier strips the class rule,
the icon stays small.
The donut / stacked-bar / rank-bar colours were already falling back
to var(--accent), so the visuals kept working — now they pick up the
proper good / info / warn / danger tones.
… tiles
Six improvements from a once-over of the metrics module:
bot_activity Primary signal becomes GrimoireLab's typed
``author_bot: true`` enrichment, with the
wildcard list kept as a should-OR fallback for
custom CI identities GrimoireLab hasn't seen.
Probe confirmed the field is populated across
every git_*_enriched doc in the demo stack.
burstiness New ``gauge`` visualisation: a fixed-range bar
spanning −1 (periodic) → 0 (random) → +1
(bursty) with a marker at the actual value. For
gimie · 5 y the marker sits at 76.5 % of the
way across (B = +0.53 → "bursty"). Methodology
note now flags the day-granularity approximation
(multiple commits in one day collapse to one
event in the Goh-Barabási inter-arrival series).
programming_ Renders as a pill cloud rather than a comma
languages string — each language becomes a discrete
badge.
project_popularity Three stat-tile cells (★ stars / ⑂ forks /
↘ dependents) with tone-coloured icons, so each
component is glanceable in its own right
instead of buried in a hyphen-separated line.
code_lines Secondary now includes ``~N lines / commit`` so
commit-size distribution is visible (lockfile
refactors and small refactors produce the same
total churn but very different per-commit
sizes). Also clarifies that ``files`` is
file-changes, not unique files.
track_total_hits Set explicitly on closure_ratio /
issue_resolution / self_merge / bot_activity so
counts stay exact past 10 000 (the default
counting cap OpenSearch applies for speed).
The filter row used ``{{ cat.name | tojson }}`` inside double-quoted
Alpine attributes, which renders as e.g.
@click="toggle("Contributor")"
:class="active.includes("Contributor") ? '' : 'is-inactive'"
The HTML parser cuts each attribute at the first inner ``"``, so
``@click`` ends up as the broken expression ``toggle(`` and Alpine
fails silently — clicking a pill did nothing on /chaoss/<repo>.
The landing page LOOKED fine because every category starts in the
``active`` array, so the all-visible state masked the missing toggles
until the user tried to actually filter.
Replace with single-quoted inner strings:
@click="toggle('Contributor')"
:class="active.includes('Contributor') ? '' : 'is-inactive'"
x-show="active.includes('Contributor')"
Safe because the four topic names ("Contributor" / "Software" /
"Lifecycle" / "Organization") contain no apostrophes. Comment in the
template explains the constraint for future authors.
…ers, fixed copy button
Two surfaces of polish on the "How we compute this" block:
Code rendering
* Registered inline highlight.js language definitions for Cypher
(clauses, :Label / :REL symbols, $params, // comments) and
SPARQL (?vars / $vars as symbols, prefixed names as attributes,
<http://…> IRIs as strings, # comments). highlight.js doesn't
ship either out of the box; the grammars are small enough to
define in-page rather than depend on an external CDN.
* Loaded the highlightjs-line-numbers.js plugin from cdnjs and
call ``hljs.lineNumbersBlock`` after each highlightElement.
Line numbers render as a muted right-aligned column with a thin
divider — the column shrinks to fit so tall queries don't waste
horizontal space.
Copy-button bug
* Old @click expression embedded ``{{ t.query | tojson }}`` inline
in a double-quoted attribute. The JSON string's own double
quotes truncated the attribute mid-expression and the rest
leaked into the button as literal text ("{ $event.target ... }, 1200)").
* Each trace card is now wrapped in an x-data scope that exposes
the query as a local ``q`` var. The button references ``q``
directly and toggles a reactive ``copied`` flag for the label
("copy query" → "copied!" via x-text). Quote-collision dodged.
Inline markdown in notes
* New Jinja filter ``md`` in chaoss/routes.py handles inline code
(both `…` and ``…``), **bold**, and *italic* with HTML escaping
first so the filter is safe to apply to anything that came out
of a store. Returns ``Markup`` so Jinja doesn't double-escape.
* Applied to result.notes, result.secondary, trace.title,
trace.result_summary, and spec.description so existing
reST-style backticks around field names (``author_name``,
``pulse:hasContribution``) finally render as <code> instead of
literal characters. New .chaoss-notes CSS gives those <code>
pills a muted-but-readable look.
Trace-block chrome
* Reorganised header into a single flex row (store pill · title ·
result_summary on the right, mono-typed). Error states get the
danger tone instead of the generic red literal.
* .chaoss-trace / .chaoss-trace-head / .chaoss-trace-pre /
.chaoss-trace-actions classes live in app.css now so the
template stays declarative.
The button on each query trace block is a deep-link into /databases that pre-loads the editor and triggers a run on arrival. "Run query" labels the *action* instead of the *destination*, which reads better in the context of a metric card where every other interactive element (filter pills, copy button) is already action-named. The hover title still describes the destination so visitors who pause on the button get the longer explanation.
Three additions, one per visible thing the dashboard was missing:
Expander chrome
The "How we compute this" disclosure used to look like plain
underlined text — easy to miss. It is now styled as a clickable
card-button: tinted background, soft border, accent-colour
chevron that rotates 90° on open, hover state that pulls the
border to the accent, and a pill-badge count of how many queries
are inside. The browser-default <summary> triangle is hidden so
the chevron is the single visual indicator.
Unification footer
Each card now ends with a "How values combine" block under the
raw queries, stating in one sentence how the per-store numbers
fold into the headline. For contributors:
"Largest non-zero of three stores: OpenSearch (windowed
git-cardinality) · SPARQL (windowed contribution graph) ·
Neo4j (all-time edges). Fallback ladder skips zeros so an
empty index can't hide a real signal."
Populated for all 22 metrics — see the slug→unification table
in chaoss/metrics.py. The text supports the same inline-markdown
backticks the notes do, so field names render as <code>.
/databases auto-run
The "Run query" button on each trace deep-links into
/databases#engine=…&q=…. consumeDeepLink() already loaded the
query into a new tab; it now also fires this.run() on the next
tick so the visitor sees the results immediately. Matches the
button label literally — "Run", not "Open".
Two pills (Software, Organization) rendered as plain underlined text
in the filter row because the assigned class names didn't exist:
Contributor → pill-info ✓ already defined
Software → pill-accent ✗ undefined — fell through to base
Lifecycle → pill-warn ✓ already defined
Organization → pill ✗ no second class, no fill
Fix:
* Define ``.pill-accent`` in app.css with a green-ish tone derived
from ``--chaoss-tone-good`` so it sits between info (blue) and
warn (amber) on the topic palette.
* Switch Organization to ``pill-neutral`` (the existing styled
gray variant) so the "no second class" gap is closed.
Also adds metric counts inside each pill:
Contributor 4 · Software 7 · Lifecycle 10 · Organization 1
The count is rendered via a new ``.pill-count`` chip — small,
faint, colour-derived-from-pill so it picks up the parent hue.
Iterating ``groups`` (only topics with ≥1 metric) instead of
``categories`` makes the counts trivially available, and ``all``
gets ``pill-neutral`` so the row stays visually consistent.
Each metric can now ship three runnable scripts that reproduce the
headline value end-to-end. The card's "How values combine" footer
gains a language-tabbed code block with python, bash, and node.js
variants — pre-filled with the exact same queries the trace blocks
display, ready to run once OPENPULSE_TOKEN is set.
* New _build_recipes(label, traces, extracts, combine) helper
auto-generates all three scripts from a per-metric extract +
combine spec. The infrastructure:
- Python: triple-quoted query strings, Python-dict literals
for OpenSearch bodies (json.dumps with True/False/None swaps).
- Bash: single-quoted heredocs so $ / " in queries are safe;
jq for response extraction.
- JS: backtick template literals for queries, indented JSON
literals for OpenSearch bodies (JSON ⊂ ES6).
Includes auth boilerplate + a tiny post() helper per language.
* Wired up for three representative metrics this round (the rest
will follow the same pattern as a follow-up):
contributors — fallback ladder (3 stores → max non-zero)
project_popularity — composite (stars/forks from SPARQL,
dependents from Neo4j)
closure_ratio — single-store ratio with state filter
* MetricResult gains an optional ``recipes`` dict[str, str] field.
Metrics that don't supply it simply skip the new footer block —
no behaviour change for the other 19 yet.
* Template renders the recipe block under "How values combine" as
a language switcher: python · bash · node.js. Each variant lives
in its own <pre><code class="language-…"> so highlight.js paints
it on first reveal. A "copy" button on the right copies the
currently-visible script via a small x-data scope (no quote-
collision bugs — we read the textContent at init time).
* CSS for .chaoss-recipe / -head / -tabs gives the block a card
look matching the trace blocks above. Active tab gets a soft
background; the code panel keeps the dark atom-one-dark surface
we use everywhere else.
Visitors can copy any script, ``export OPENPULSE_TOKEN=…``, and
re-derive the dashboard number locally — same queries, same
unification logic, same result.
…der)
The methodology block is no longer a <details>/<summary> expander
that pushed neighbouring cards around when opened. Each card now
has a button that opens a proper modal overlay containing the same
content: traces, unification footer, recipes.
Why a modal:
* Tall queries (especially the OpenSearch DSL bodies on bot_activity
or absence_factor) made the inline expander dwarf its neighbour
cards in the grid.
* The dashboard is a glanceable grid; the methodology is a deep-dive
surface. Putting them in different visual planes matches the
information hierarchy.
* Bigger viewport for the queries — 90vh × 920px max — lets the
code blocks breathe and the recipe tabs sit comfortably.
How it works:
* The card trigger is now a <button class="chaoss-trace-trigger">
with a magnifier-glass SVG, the same "How we compute this · N
queries" label, and the existing card-button styling.
* Click fires an Alpine x-data scope's show() method, which sets
open=true and adds ``chaoss-modal-lock`` to <body> (prevents
page-behind scroll jacking).
* The modal is rendered via x-teleport="body" so its z-index sits
cleanly above every card and is never clipped by a parent's
overflow.
* Close paths: ESC key (@keydown.escape.window), backdrop click
(@click.self on backdrop), explicit × button. All three call
hide() which removes the body class so scroll resumes.
* highlight.js + the line-numbers plugin run in the show()
handler's $nextTick — by then the panel is in the DOM and the
<pre><code> elements are paintable.
Modal chrome:
* Header carries the topic pill (Contributor / Software / …) and
the metric name, so visitors who opened the modal from one card
don't have to context-switch to remember which metric they're
looking at.
* The CHAOSS-spec question (e.g. "Is there a community at all?")
sits as a muted lede paragraph above the trace cards — small
framing that the expander couldn't fit cleanly.
* .chaoss-modal-backdrop dims everything behind it (rgba .55 +
backdrop-blur 2px). The panel itself uses the existing
--surface palette so light + dark themes both look native.
Earlier commit shipped recipes for 3 of 22 (contributors,
project_popularity, closure_ratio). This pass fans the same
infrastructure out to all metric compute functions so every card on
the dashboard can hand the visitor a runnable script.
Single-trace + scalar extract:
technical_fork SPARQL forks count (Neo4j fallback)
licenses_declared SPARQL license IRIs → presence flag
academic_impact SPARQL row count of shared-author articles
activity_dates OpenSearch date_histogram bucket sum
org_diversity SPARQL distinct org names
programming_languages SPARQL distinct languages
first_response OpenSearch percentile P50
issue_resolution OpenSearch percentile P50
cr_reviews OpenSearch filter agg doc_count
Ratio metrics:
self_merge self-merged / total-merged
bot_activity bot-flagged commits / total
Multi-step / client-side compose:
code_lines sum(added) + sum(removed) from two aggs
project_demographics terms-agg bucket count
absence_factor walk descending buckets to 50% cumulative
burstiness daily histogram → inter-arrival days → B
new_contributors SPARQL row count + OpenSearch buckets
filtered client-side against cutoff_ms
Shared issues helper (_issues_count): drives issues_new,
issues_active, issues_closed — single hits.total.value pull.
Each metric's combine logic mirrors what the Python compute function
does at runtime, so the bash / python / js recipes all converge on
the same headline value when re-run by hand.
Empty data states: the 9 metrics whose underlying store isn't
populated for the demo repo (the github_*_enriched-backed ones for
sdsc-ordes/gimie) hit no-data early returns. Each function now
declares its recipe variable as ``None`` at the top so the early
returns evaluate cleanly and the template silently skips the recipe
block — those metrics will ship recipes the moment they have data
to reproduce.
Implementation:
* Per-metric extract+combine specs live in /tmp/patch_recipes.py
(the one-shot script that generated the injections). The script
walks the file, locates the happy-path return per function via a
"last slug marker" heuristic, injects the _build_recipes(...)
call right before the return, and adds ``recipes=<var>,`` to
every MetricResult call in the function.
* 41 returns patched total (22 happy-paths + 19 no-data fallbacks).
Rebuilds the methodology modal with a clear top-to-bottom flow and
opt-in detail blocks so it stops being a wall of code:
1. Lede — the CHAOSS question rendered as an italic blockquote,
left-bordered in the topic-info accent. Sets the frame the
metric is trying to answer.
2. Methodology card — always-visible explanation. Combines the
metric's description (catalogue blurb) with its result.notes
(per-call methodology paragraph) and the unification statement
inside a callout. Markdown `inline-code` survives end-to-end
so reST-style ``field_name`` references render as <code> chips.
3. Queries section — one collapsible expander per trace, all
closed by default. The expander head shows the store pill, the
trace title, and the result-summary line at a glance; the body
unfolds to reveal the syntax-highlighted query plus Run / copy
buttons. Lead-in paragraph tells the visitor what these queries
are and where to re-run them.
4. "Reproduce end-to-end" — same expander pattern, also closed.
Inside: the python / bash / node.js tabs we already had, with
a lead-in paragraph explaining the OPENPULSE_TOKEN setup.
Codeblock readability:
* Bumped base font to 12.5 px + line-height 1.55. Easier on the
eye, especially for SPARQL with its long IRIs.
* Line-number gutter restyled: tabular-nums for alignment, muted
rgba colour, faint divider, subtle background tint, no longer
bleeds into the code column.
* Code wraps only on narrow viewports (≤ 720 px); desktop keeps
horizontal scroll so query indentation stays intact.
* Token colours overlaid on atom-one-dark to pull our domain
tokens out: :Labels and :REL symbols are blue, ``?var`` and
``$var`` SPARQL bindings are yellow, prefixed names
(``pulse:hasContribution``) are cyan, keywords stay purple,
strings green, comments italic-grey. Plays nicely with both
the default atom-one-dark colours and our --chaoss-tone palette.
Modal chrome:
* Modal body now uses flex-column with gap-16 so the four
sections breathe without hand-rolled margins.
* Section titles are uppercase eyebrow labels (11 px, letter-
spacing 0.6) with a count pill — same look as the homepage
section headers, so the modal feels like a deeper drilldown
of the same surface, not a separate UI.
* Trace expanders use a chevron that rotates 90° on open; same
pattern for the recipe expander. Single visual language for
"this opens to show more".
Several route modules ship their own Jinja2Templates instance because
they need module-local filters or registered their own directory
historically:
* chaoss/routes.py — needs the ``md`` inline-markdown filter
* routes/hub.py — predates the central templates singleton
Both built their template envs independently of the one in main.py,
and therefore never saw the shared globals that base.html's sidebar
reads:
* templates.env.globals["dashboards"] → 3-entry list rendered as
the Dashboards group (Neo4j Browser / Oxigraph / OpenSearch)
* templates.env.globals["ontology_url"] → external ↗ link below
the Hub entry
Result: both /hub and /chaoss surfaces silently rendered an empty
"Dashboards" section plus a broken (href="") ontology link in their
sidebar — exactly the bug the user just flagged.
Fix is a tiny propagation helper in main.py that copies every key
from the central globals dict onto each foreign Jinja env after they
are imported. ``setdefault`` preserves any per-instance overrides
(none today; future-proofs against a route registering its own
``dashboards`` for some reason).
Verified — all five surfaces now show three dashboard links and the
ontology link in the sidebar:
/ → 3 dashboard links
/hub → 3 dashboard links
/chaoss → 3 dashboard links
/databases → 3 dashboard links
/chaoss/github.com/sdsc-ordes/gimie → 3 dashboard links
…ange
Two things landing together because they both hit the same surface:
Chart.js 'fullSize' error
Clicking 7d / 30d (any wider range) raised the runtime error
"Cannot set properties of undefined (setting 'fullSize')" in
the marquee chart row. Root cause: chart.update('none') on a
time-scale chart whose data range shifts by a couple of orders
of magnitude (1h → 7d = 168× more points spanning 168× the
window) corrupts Chart.js's internal layout state — its next
box-resize pass walks an undefined chart.boxes[i].fullSize.
Fix: when the visitor switches range, destroy every chart and
recreate from scratch instead of updating in place. We track
``_lastRange`` so the no-op silent-polling case (same range,
just a fresh sample) keeps the cheap update path. Polling also
wraps the in-place update in try/catch with destroy+rebuild as
a defensive fallback for any other Chart.js layout glitch.
Custom date range
New ``custom`` entry in the range buttons. Selecting it reveals
two ``<input type="datetime-local">`` fields and an Apply
button; the values persist in localStorage so a refresh keeps
the chosen window. ``applyCustom()`` converts the local naive
values to ISO-UTC with .toISOString() and hits the API with
``?range=custom&start=...&end=...``.
A small bucket heuristic for the custom path: targets ~150
points per chart so wide custom windows don't blow up the
chart rendering (e.g. 30 days at 1-minute granularity = 43k
points is unusable; 150-point bucketing → roughly 5-min
samples for a 12-hour window, 4-hour samples for a 30-day one).
Server side
/api/stats/history gains optional ``start`` + ``end`` query
params. When ``range=custom`` and both are provided, the SQL
swaps from ``ts > datetime('now', cutoff)`` to ``ts BETWEEN
start AND end``. Bucket downsampling logic stays the same.
UX nice-touches
* Auto-polling is paused when range='custom' — the user
explicitly pinned that window, no point silently shifting
its data on them.
* If localStorage left them on range=custom but the
customFrom/customTo pair is empty, init() falls back to '6h'
so the first load doesn't 400.
* The Apply button is disabled until both date inputs have a
value.
The sidebar brand block used to read ``open-pulse`` (kebab) + a
``hub`` tag and was non-interactive. Two small tweaks:
* Display name → "OpenPulse" (product name as marketed, mixed-case
rather than the kebab repo-slug).
* Brand block becomes an ``<a href="/">`` so the title is also a
"back to home" affordance — the convention every web app gets
away with by default.
The ``hub`` tag stays since the sidebar lives inside the hub
surface specifically.
Two issues on the Overview facets row:
* The Discipline facet rendered empty because its query asked for
schema:applicationCategory, but the open-pulse extractor writes
discipline triples as pulse:discipline. We confirmed against the
live SPARQL store:
pulse:discipline → 2 730 triples / 15 distinct
schema:applicationCategory → 0 triples
Switch the values_query + predicate_path to pulse:discipline.
Discipline now reads: 15 values, top is Wikidata Q428691
(Genetics) at 2 179 repos.
* Added the ``pulse:`` prefix to the shared PREFIXES block in
queries.py so any future facet pointing at the open-pulse
ontology doesn't have to redeclare it.
* The count pill in each facet card showed ``8650 values`` —
user-requested it be just the number. Dropped the ``+ ' values'``
string suffix on the x-text binding.
Keyword stays a defined facet but renders ``no values yet`` — the
data plane simply doesn't carry schema:keywords triples today
(extractor doesn't emit them). Once the extractor lands keyword
support, that card will populate automatically.
The Organization card showed 8 650 "values" because the underlying
query took the schema:name of every (schema:author|schema:publisher)
target — and the data plane points every author at a schema:Person
node (8 634 of them). The top of the list was full of individual
contributors like Panaetius, Ralf Grubenmann, Rok Roškar — clearly
not organisations.
Switched the facet to the right predicate + type filter:
?repo pulse:ownedBy ?org .
?org a org:Organization ;
schema:name ?value .
Now returns 109 actual orgs: EPFL-ENAC (164), SwissDataScienceCenter
(115), sdsc-ordes (88), c4dt (7), eawag-surface-waters-research (4),
etc.
Also added a sibling "Owner (personal)" facet for the 225 schema:Person
nodes that own repos — GitHub user accounts whose repos are personal
rather than under an org. Same shape, different type filter. Lets the
overview surface the two ownership flavours separately instead of
collapsing them.
Added the ``org:`` prefix to the shared PREFIXES block so future
facets touching the W3C org vocab don't have to redeclare it.
…acet
The Discipline facet's values were rendered as raw Wikidata IRIs
(Q428691, Q12483, …) because the open-pulse data plane stores them
as opaque references — there are no local labels in our SPARQL store
to dereference.
Fix: at facet-load time, batch every Wikidata Q-id encountered across
all facets into a single ``wbgetentities`` call to Wikidata's
MediaWiki API, with English labels. Results are cached in a
module-level dict so subsequent facet loads cost nothing.
* ``_decorate_wikidata_labels()`` runs after ``_facet_values()``
returns for every facet. It walks every value, collects unmapped
Q-ids, fires one batched request per 50 ids (API limit), and
attaches a ``label`` field to each value in place.
* Failure is non-fatal — if Wikidata is unreachable, values keep
their IRI-only state and the UI falls back to the IRI tail.
* Module-level ``_WIKIDATA_LABEL_CACHE`` survives between requests
so the network call only fires when a new Q-id appears.
Frontend: ``shortLabel()`` now accepts a value object (with optional
``label``) instead of a raw string, so the existing facet rows pick
up the resolved label automatically. Q428691 → "Open Source Software"
(or whatever Wikidata's English label is) instead of "Q428691".
The Discipline facet's Wikidata-IRI → label resolver returned 403 on every call from inside the container even though plain ``curl`` was fine. Cause: httpx's default ``python-httpx/X.Y`` UA is rejected by the MediaWiki action API per meta.wikimedia.org/wiki/User-Agent_policy. Set a compliant UA with tool + contact info: open-pulse-hub/1.0 (+https://github.com/sdsc-ordes/open-pulse; open-pulse@epfl.ch) httpx Result: labels now resolve. Q428691 → "computer engineering", Q12483 → "statistics", and so on.
The old ``repo_type`` facet was rdf:type filtered to schema.org classes — which on this dataset always pulls Person / SoftwareSourceCode / ScholarlyArticle, a count of how many of each graph entity exists rather than a useful breakdown of what the repositories *are*. Switch to ``pulse:repositoryType`` (the artefact-kind tag the extractor writes per repo). Returns the four buckets the open-pulse ontology defines: Software 1 943 Documentation 165 EducationalResource 68 Data 37 Also: ``shortLabel()`` now strips the ``#fragment`` from ontology IRIs so the values render as "Software", "Documentation", … instead of the full IRI. The full IRI is still in the ``title=`` tooltip for citation.
The Keyword facet has been empty since launch — the open-pulse extractor doesn't emit ``schema:keywords`` triples, so the card always read "no values yet". Pulling it for now; we can bring it back the moment the extractor lands keyword support. Replacing the slot with an Organization-type facet keyed off ``pulse:OrganizationType``. Counts repos (not orgs) so the buckets read as "how much of the dataset is owned by which kind of organisation", e.g.: ResearchInstitution 402 OtherOrganizationType 378 University 167 SoftwareProject 147 PrivateCompany 23 NonProfitOrganization 14 CommunitySpace 6 GovernmentAgency 5 The shortLabel() ``#fragment`` strip we added for repositoryType also handles these IRIs cleanly without further work.
…pens
The methodology modal was running highlight.js + the line-numbers
plugin once at modal-open time, on every <pre><code> in the modal —
but those code blocks live inside per-trace ``x-show="open"``
expanders that start collapsed (``display: none``). The line-numbers
plugin uses a CSS table layout that depends on the element's
rendered dimensions: on a hidden element the plugin produces an
empty/misaligned gutter and aborts, which on some browsers also
leaves the highlight pass in a half-applied state — net effect for
the visitor: no colours and no line numbers.
Fix: defer highlighting to the moment each expander actually opens.
* New ``window.chaossHighlight(container)`` helper in repo.html,
co-located with the cypher + sparql language registrations. It
walks every pre code inside the given container, calls
``hljs.highlightElement`` then ``hljs.lineNumbersBlock``,
swallowing any per-block exception so one bad query doesn't take
out the rest.
* Each trace expander now flips a local ``painted`` flag the first
time its ``open`` goes true and, on the next tick, calls
``chaossHighlight($refs.body)`` — the trace's own body
container, so we only paint what we just revealed.
* The recipe ``<details>`` expander follows the same pattern via
``@toggle`` + ``$el``.
* The parent modal's ``show()`` no longer touches highlight.js; it
just flips open + body-scroll lock. Painting is owned by each
sub-expander.
Net result: open a metric card → modal flies in unchanged → click
"Neo4j · All-time distinct contributors" → the cypher block now
paints (purple keywords, blue ``:Label`` symbols, yellow comments,
proper line-number gutter on the left). Same for SPARQL and the
OpenSearch-DSL JSON. Same for "Reproduce end-to-end" when its
``<details>`` opens.
The query codeblocks were locked to a hardcoded dark theme — the
atom-one-dark CSS was loaded from cdnjs unconditionally, and our
overrides set #1a1d21 backgrounds + atom-one-dark token colours
directly. On the light hub theme the result was a black-on-black
gap in the middle of every modal.
Replace the hardcoded palette with a ``--chaoss-code-*`` CSS
variable family, defined once per theme via the same ``[data-theme]``
selector the rest of the hub uses:
--chaoss-code-bg backdrop #1a1d21 / #fafafa
--chaoss-code-fg default text #c5ccdb / #383a42
--chaoss-code-keyword MATCH SELECT PREFIX purple
--chaoss-code-string quoted literals green
--chaoss-code-number numerics orange
--chaoss-code-comment // and # comments grey italic
--chaoss-code-symbol cypher :Label / :REL blue
--chaoss-code-variable sparql ?var / $var yellow / brown
--chaoss-code-attribute json key, pulse:hasFoo cyan / teal
--chaoss-code-title function / type names red
--chaoss-code-ln-* gutter background, fg, rgba black / rgba white
border colour
+ a literal slot (true/false/null) that mirrors ``number``
Dropped:
* atom-one-dark.min.css CDN link on /chaoss and /databases — the
full palette now lives in our own CSS, so we're not loading
bytes whose colours only work in one theme.
Added overrides so the same rules cover the recipe expander's
<pre> blocks too — they're rendered with ``language-python`` /
``language-bash`` / ``language-javascript`` and share the same
token classes.
Both surfaces (CHAOSS modal traces + /databases console snippet)
now repaint cleanly when the user flips the theme toggle.
The recipe block previously had three separate <pre> elements (one
per language) with their own CSS rules, separate padding, and no
line-numbers gutter. Side-by-side with the trace expanders above —
which used the chaoss-trace-pre chrome with a 14 px line-number
column on the left — the recipe's text started flush at 16 px
padding, so the two codeblocks felt visually misaligned inside the
same modal.
Refactor to a single reactive <pre class="chaoss-trace-pre">:
* Three pre/code blocks → one. Content + language class flip via
x-text / :class as ``lang`` changes.
* ``scripts`` is now a JSON-encoded dict passed straight into
x-data, not read from $refs.recPy/Bash/Js textContent. Cleaner
Alpine scope.
* Local ``repaint()`` method re-runs hljs.highlightElement + the
line-numbers plugin on the now-visible <code> after every tab
switch, so each language gets its own painted gutter without
the previous "first paint sticks" bug.
* Sharing the chaoss-trace-pre class means the recipe and the
trace codeblocks pick up the same theme-aware token palette,
line-number gutter colours, max-height behaviour, and padding
automatically — no separate selectors to keep in sync.
The only recipe-specific tweaks live in a tiny .chaoss-recipe
.chaoss-trace-pre selector: a taller max-height (recipes are
multi-screen scripts, not single SPARQL queries) and ``border-top:
0`` because the recipe-head already provides that divider.
The marquee strip used to show only the current value next to each
metric (services healthy / running / sparql repos / neo4j nodes /
neo4j rels) — useful for the live snapshot but no way to tell at a
glance whether the number is growing, flat, or recovering from a
dip. Adding tiny inline-SVG polyline sparklines next to those five.
* marqueeBar() now drives two refresh loops: ``refresh()`` every
10s (current values, same as before) and ``refreshHistory()``
every 60s (sparkline data).
* refreshHistory hits ``/api/stats/history?range=6h&bucket_seconds=600``
— 36-point series the chart on /overview also uses — and turns
each numeric series into a pre-baked ``points`` string scaled
to the 60×14 viewBox. refresh() reads the cached strings on
every tick so the 10s heartbeat re-renders without re-fetching.
* Sparklines are rendered only when the item carries a ``spark``
field, so the three string items (longest uptime, last update,
services as "X/Y healthy" label) stay plain.
* Template uses a 60×14 SVG with a single <polyline> drawing the
series in ``currentColor`` — the colour matches the parent
marquee-item so the line reads as a quiet trend rather than a
competing focal point. Opacity 0.72 to keep it secondary to
the digits.
…tions)
The previous query walked the Path-2 affiliation chain (Person ->
org:hasMembership -> Org -> schema:name) and produced noisy counts
because contributor self-declared institutional names are not yet
canonicalised upstream of SortingHat. For sdsc-ordes/gimie that gave
10 "distinct" orgs that included two near-duplicate spellings of SDSC
("Swiss Data Science Center" / "Centre") and a same-acronym collision
with the San Diego Supercomputer Center.
Path 1 — pulse:ownedBy — uses GitHub-issued organisation handles, so
the same node is the same node by construction. Rewriting the metric
to count distinct ownership orgs reachable from the repo's
contributors:
?person pulse:hasContribution / pulse:contributionTo
?otherRepo pulse:ownedBy ?org .
?org a org:Organization .
For gimie: 6 clean orgs — sdsc-ordes (10 contribs), TopQuadrant (3),
odtp-org (2), Imaging-Plaza (2), SwissDataScienceCenter (1),
EPFL-ENAC (1). All GitHub handles, no canonicalisation work needed.
The metric's notes + unification text now spell out that this is
Path 1 only, and flag the upstream canonicalisation gap so a future
maintainer knows when it's safe to switch back to / add Path 2.
New programmatic surface — same data the dashboard renders, exposed
as JSON for callers that don't want to scrape HTML fragments.
GET /api/chaoss/v1/topics
4 topic groups (Contributor / Software / Lifecycle / Organization)
with metric counts.
GET /api/chaoss/v1/metrics (?category=X)
Catalogue of every metric spec. Pure static data, no upstream
stores touched. Optional ?category filter.
GET /api/chaoss/v1/metrics/{slug}
One metric spec by slug. 404 on unknown.
GET /api/chaoss/v1/repositories/github.com/{owner}/{repo}/metrics
(?window=, ?category=, ?include=traces,recipes,series)
Compute every metric for a repo. Per-metric upstream errors
don't 500 the request; the metric reports "—" and the trace
carries the error.
GET /api/chaoss/v1/repositories/github.com/{owner}/{repo}/metrics/{slug}
Compute a single metric. Same payload shape as one element
from the all-metrics endpoint.
Implementation details:
* Path keeps ``github.com/`` as an explicit segment so a future
``gitlab.com/...`` resolver slots in without breaking existing
clients. Versioned under ``/v1`` so a breaking change later
bumps to ``/v2`` without disturbing what's deployed.
* ``include`` query param is opt-in: traces, recipes and series are
omitted by default. Without them the full 22-metric response is
~25 KB; with ``include=recipes`` it jumps to ~114 KB (recipes are
three full scripts per metric × 22).
* ``_spec_to_dict``, ``_trace_to_dict`` and ``_result_to_dict``
serialise the MetricSpec / QueryTrace / MetricResult dataclasses
into stable JSON shapes. Recipe-builder output is already plain
dict[str, str], passed through.
* ``computed_at`` is an ISO-8601 timestamp (seconds precision) so
callers can tell whether they're looking at fresh data.
* Auth via the same ``maybe_require_auth`` gate as the existing
routes — public when ``HUB_PUBLIC_KNOWLEDGE=true``.
The existing HTML fragment route at
``GET /api/chaoss/github.com/{owner}/{repo}/{slug}`` is untouched —
the dashboard's lazy-load skeletons keep calling it as before.
… PR lifecycle)
Total goes from 22 → 28 metrics. Contributor 4 → 6, Lifecycle 10 → 14.
Contributor cadence:
inactive_contributors Distinct authors whose most-recent commit
predates the window. Mirrors the "dormant"
bucket from project_demographics as a
standalone metric. Donut tone goes
danger when inactive share > 70 %.
occasional_contributors Distinct authors with ≤ 4 commits in the
window — CHAOSS's drive-by threshold.
For gimie · 5y: 6 of 16 active contribs.
PR lifecycle (use github_*_enriched, light up on stacks with the
GrimoireLab GitHub backend ingested):
cr_accepted Merged PRs in window (acceptance half of
closure_ratio). Monthly sparkline.
cr_declined Closed-not-merged PRs. The other half;
pair with cr_accepted for accept/reject
split. Monthly sparkline.
cr_duration P50 / P90 days from PR open to MERGE for
PRs that shipped. Acceptance-speed view.
pr_time_to_close P50 / P90 days from PR open to CLOSE for
any closed PR (merged + declined).
Differs from cr_duration: a wide gap
between the two means the project closes-
without-merging slowly.
All six ship with python / bash / js recipes via _build_recipes; two
shared helpers (_pr_count_metric, _pr_percentile_metric) capture the
boilerplate for the four PR-lifecycle metrics.
Conflict: src/open_pulse/gui/hub/main.py
Both branches added new routers / imports to the FastAPI app:
· develop → ``admin`` router (the /admin Resources panel from
PR #51, commit 915151f)
· this branch → ``hub`` + ``chaoss_routes`` routers + the
_propagate_globals helper that mirrors the shared
template env to instances with their own filters
Resolved by keeping all three. New combined wiring:
app.include_router(crawler.router)
app.include_router(admin.router) # develop
app.include_router(hub.router) # ours
app.include_router(hub.api) # ours
app.include_router(chaoss_routes.router) # ours
base.html merged cleanly (the new ``/admin`` nav entry from develop
sits alongside the new ``/chaoss`` nav entry we already added).
The CI Baseline pre-commit job flagged 9 lint warnings (unused
imports, ambiguous ``l`` variable names, one stray f-string) and
16 files that needed ``ruff format`` reflow. Fixed everything in
one pass:
· ruff check --fix (auto-fixable): 6/9 cleared
· manual: 3 remaining E741s renamed
``l`` → ``lic`` / ``lang``
· ruff format: 16 files reformatted
No behaviour change. CHAOSS dashboard + v1 API + Overview all still
serve HTTP 200 on the smoke routes after the rebuild.
- Wrap contact emails in <…> autolinks so MD034 stops flagging them - Drop the trailing newline from chaoss/__init__.py — end-of-file-fixer wants this empty file truly empty (0 bytes)
# Conflicts: # src/open_pulse/gui/hub/main.py
`frontier_extend` (added on develop in ee9dfbe) defaults to `enabled=False` like `apply_grimoire_projects`, so the "all enabled" test needs to flip it on too — otherwise build_tasks() drops it and the equality against STEP_NAMES fails.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three intertwined surfaces shipped together: the /hub entity browser (every URL gets a page backed by Neo4j + SPARQL + OpenSearch + Qdrant), the /chaoss dashboard (22 metrics per repository, grouped by official CHAOSS topic, every value reproducible end-to-end), and an Overview facet polish.
Changes
/hub knowledge surface
HUB_PUBLIC_KNOWLEDGE.?chain=+ localStorage; hover preview tooltips on backlinks/related./chaoss dashboard (22 metrics)
DEPENDS_ONdependents), programming_languages, activity_dates, closure_ratio, org_diversity.author_bot:true+ wildcards).OPENPULSE_TOKENneeded to run.--chaoss-code-*variables; syntax + line-numbers paint when each expander opens; recipe block shares chrome with trace blocks.mdJinja filter renders backticks in notes as<code>chips.Overview polish
'fullSize'error on range switch fixed (destroy + recreate on range change).from/todatetime inputs;/api/stats/historyacceptsstart+endISO params.pulse:discipline; Wikidata IRIs resolved to human labels (batched, cached, compliant UA).org:Organization(drops 8 634 schema:Person entries); new "Owner (personal)" sibling facet for user-owned repos.pulse:repositoryType(Software / Documentation / EducationalResource / Data).pulse:OrganizationTypeenumeration.Cross-cutting fixes
/databasesdeep-link from CHAOSS auto-runs the query on arrival.Test plan
github.com/sdsc-ordes/gimie, all panels lazy-load.