183 lines
7.4 KiB
Markdown
183 lines
7.4 KiB
Markdown
# Piscine Graph Tweaks
|
||
|
||
Firefox extension for the EPITA piscine exercise graph (`exercises_c`,
|
||
`exercises_shell`, …) on `intra.forge.epita.fr`. Two things:
|
||
|
||
**A progress panel**, bottom right of the page:
|
||
|
||
- **validated / total** with the percentage and a segmented bar;
|
||
- a separate **required** count, which is the one that actually matters;
|
||
- breakdown: validated, required left, bonus left, locked.
|
||
|
||
**A full screen view of the graph**, because the page crams a ~5000px-wide
|
||
diagram into a 200px-tall box:
|
||
|
||
- opens at a readable zoom instead of shrinking everything to fit;
|
||
- **search** — dims everything but the matches, and recentres when there is only one;
|
||
- **minimap** bottom right, coloured by status; click or drag it to jump anywhere;
|
||
- **arrow keys** to pan (Shift for 3×), wheel to zoom, shift+wheel to pan sideways,
|
||
drag to pan, double-click to zoom in, click a node to open the exercise;
|
||
- `/` search · `Home` readable view · `0` fit everything · `+` / `−` zoom · `Esc` close.
|
||
|
||
No network request is made: everything is read from the page itself.
|
||
|
||
## How it reads the graph
|
||
|
||
The graph is a mermaid `stateDiagram` rendered as inline SVG, and each node keeps
|
||
its whole state in its `id`:
|
||
|
||
```
|
||
state-"_required=true/_validated=true/_accessible=true/…/exercises_c/clang~format"-0
|
||
```
|
||
|
||
So the three booleans are parsed straight from the id — no color guessing — and
|
||
`~` is mermaid's escape for `-` in the path. Status mapping:
|
||
|
||
| `_validated` | `_accessible` | `_required` | status |
|
||
|--------------|---------------|-------------|---------------|
|
||
| `true` | – | – | validated |
|
||
| `false` | `false` | – | locked |
|
||
| `false` | `true` | `true` | required left |
|
||
| `false` | `true` | `false` | bonus left |
|
||
|
||
Verified against the live page: 91 nodes parsed, the only skipped element being
|
||
mermaid's `state-root_start-1` marker.
|
||
|
||
## Why there is no "started but unfinished" state
|
||
|
||
There is none in the page. Each node carries exactly those three booleans, and
|
||
`_validated` only flips once everything passes. An exercise started this morning
|
||
and one never opened produce **byte-for-byte identical markup** — compare
|
||
`gdb_wristwatch` and `test_a_bit`, both `_required=true/_validated=false/
|
||
_accessible=true`. So per-exercise completion cannot come from this page, and the
|
||
extension does not pretend otherwise.
|
||
|
||
If you ever want real "started" detection, it has to come from the GitLab API
|
||
rather than the graph — one call listing the exercise projects and their
|
||
`last_activity_at` would say which ones you have pushed to:
|
||
|
||
```
|
||
/api/v4/groups/<url-encoded group path>/projects?per_page=100&order_by=last_activity_at
|
||
```
|
||
|
||
That is one request for the whole graph, not one per exercise. Not wired in: it
|
||
needs checking against the real API response first.
|
||
|
||
## How the full screen view works
|
||
|
||
It **moves** the real `<svg>` into the overlay rather than cloning it (a
|
||
placeholder holds its spot in the page), so mermaid's own `<style>` block — which
|
||
lives inside the SVG and is scoped to its id — keeps working untouched. It then
|
||
takes over the `svg-pan-zoom` viewport `<g>` transform instead of fighting that
|
||
instance. Inherited CSS custom properties (`--required-validated` and friends) do
|
||
not survive the move, so they are resolved and re-declared on the overlay first.
|
||
Closing puts every attribute back exactly as it was.
|
||
|
||
---
|
||
|
||
## Installing (for you, while developing)
|
||
|
||
1. Open `about:debugging#/runtime/this-firefox`
|
||
2. **Load Temporary Add-on…**
|
||
3. Pick this folder's `manifest.json`
|
||
4. Open the graph page — the panel shows up bottom right
|
||
|
||
"Temporary" means it **disappears when Firefox restarts**. That is the
|
||
development mode: after each code change, hit **Reload** on `about:debugging`.
|
||
For a permanent install, use the same route as your friends below.
|
||
|
||
## Sharing it with friends (without publishing to the store)
|
||
|
||
Build the package:
|
||
|
||
```sh
|
||
./build.sh # -> dist/piscine-graph-tweaks-<version>.zip and .xpi
|
||
```
|
||
|
||
Then pick one of three routes. The catch: **Firefox release refuses to install an
|
||
unsigned extension permanently**, so a `.xpi` dropped in a Discord channel will
|
||
not install by double-click.
|
||
|
||
### Option A — the zip plus temporary loading (simplest, zero setup)
|
||
|
||
Send the `.zip`, they unpack it, then `about:debugging` → *Load Temporary
|
||
Add-on…* → `manifest.json`.
|
||
|
||
- works on any Firefox, right away
|
||
- has to be redone every time the browser restarts
|
||
|
||
### Option B — "unlisted" signing on AMO (best for daily use)
|
||
|
||
Mozilla signs the extension **without publishing it** in the catalogue: it stays
|
||
unlisted, you distribute the `.xpi` yourself, and it installs permanently on a
|
||
normal Firefox.
|
||
|
||
1. Create an account on https://addons.mozilla.org, then generate an API key at
|
||
https://addons.mozilla.org/developers/addon/api/key/
|
||
2. Sign it:
|
||
|
||
```sh
|
||
npm install --global web-ext
|
||
web-ext sign --channel=unlisted \
|
||
--api-key="$AMO_JWT_ISSUER" --api-secret="$AMO_JWT_SECRET"
|
||
```
|
||
|
||
3. You get a **signed** `.xpi` in `web-ext-artifacts/`. Friends open it with
|
||
Firefox (drag into the window, or `Ctrl+O`) → *Add*.
|
||
|
||
The extension id is already pinned in `manifest.json`
|
||
(`piscine-graph-tweaks@valentin`) — required for signing, and it must not change
|
||
after the first signature.
|
||
|
||
### Option C — Firefox Developer Edition / Nightly / ESR
|
||
|
||
On those builds only, signature enforcement can be turned off: `about:config` →
|
||
`xpinstall.signatures.required` → `false`, then install the unsigned `.xpi`.
|
||
Pointless on Firefox release, where the pref is ignored.
|
||
|
||
---
|
||
|
||
## Layout
|
||
|
||
```
|
||
manifest.json extension manifest (MV3) and the matched URLs
|
||
src/content.js node parsing, stats, the progress panel
|
||
src/viewer.js full screen view: pan/zoom, search, minimap
|
||
src/panel.css every injected style
|
||
src/inject.js injects the network hook into the page world
|
||
src/net-hook.js records what the page fetches (discovery aid)
|
||
src/icon.svg icon
|
||
tools/dump-graph.js console snippet: parsed nodes, works without the extension
|
||
tools/make-fixture.js console snippet: captures the graph for offline testing
|
||
tests/build-harness.js rebuilds tests/harness.html from a fixture + current src
|
||
tests/shot.sh screenshots the harness with headless Firefox
|
||
build.sh produces dist/*.zip and dist/*.xpi
|
||
```
|
||
|
||
`src/viewer.js` must load **before** `src/content.js` — the panel's button calls
|
||
`window.__pgtViewer`.
|
||
|
||
## Testing
|
||
|
||
Two ways, both offline-friendly:
|
||
|
||
- **Against a captured page.** Run `tools/make-fixture.js` in the console on the
|
||
graph page, save it with `wl-paste > tests/fixture.json`, then
|
||
`node tests/build-harness.js && tests/shot.sh`. The harness inlines the current
|
||
`src/` so any edit is one command away from a screenshot.
|
||
- **Against the live page.** Anything driving a real browser works; the parser and
|
||
both screenshots in this repo's history were checked that way.
|
||
|
||
## Other projects
|
||
|
||
The manifest already matches `*/root/exercises_*` on `intra.forge.epita.fr`, so
|
||
`exercises_shell` and any future `exercises_python` work with no change. The panel
|
||
title is derived from the project segment of the node ids.
|
||
|
||
## If the numbers look wrong
|
||
|
||
The graph generator would have to have changed. Open the console on the page and
|
||
run `__pgtDump()`: it prints the parsed nodes and the raw HTML of the first one,
|
||
which is what `RE_STATE_ID` in `src/content.js` needs to match. `__pgtNet()` lists
|
||
what the page fetched, in case richer data ever appears.
|