Skip to content

Instantly share code, notes, and snippets.

@Stanislas-Poisson
Last active May 5, 2026 07:36
Show Gist options
  • Select an option

  • Save Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855 to your computer and use it in GitHub Desktop.

Select an option

Save Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855 to your computer and use it in GitHub Desktop.
TamperMonkey StimData

Tampermonkey Scripts – STIMDATA Toolkit

Collection de scripts Tampermonkey destinés à améliorer la productivité sur les outils internes STIMDATA.

Ces scripts ajoutent des fonctionnalités autour de :

  • GitHub (suivi des Pull Requests)
  • StimTrack (améliorations UI et export rapide)

Prérequis

Ces scripts nécessitent l’extension Tampermonkey installée dans votre navigateur.

Installation : https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=fr


Configuration Tampermonkey

Après installation de l’extension :

  1. Ouvrir Tampermonkey

  2. Aller dans le Dashboard

  3. Vérifier que l’extension est activée

  4. Autoriser l’exécution des userscripts sur les domaines concernés :

    • github.com
    • stimtrack.stimdata.tools

Sans cette configuration, les scripts ne s’exécuteront pas correctement.


🚀 Installation (RECOMMANDÉE)

Les scripts peuvent être installés directement via URL depuis le dashboard Tampermonkey :

Étapes

  1. Ouvrir Tampermonkey
  2. Aller dans l’onglet Utilities
  3. Descendre tout en bas jusqu’à “Install from URL”
  4. Coller une des URLs ci-dessous
  5. Valider l’installation

📦 Liens d’installation

🔹 GitHub PR Dashboard

https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-GITHUB.js

🔹 StimTrack Enhancer

https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-StimTrack.js

Contenu

1. GitHub PR Dashboard

Fichier : TamperMonkey-StimData-GITHUB.js

Ce script ajoute un dashboard directement sur les pages GitHub de l’organisation STIMDATA.

Fonctionnalités

  • Récupération des PR ouvertes via l’API GitHub
  • Filtrage sur l’organisation STIMDATA
  • Groupement des PR par repository
  • Affichage des PR où l’utilisateur est impliqué
  • Détection d’un identifiant externe dans le titre de la PR (format #12345)
  • Génération automatique d’un lien vers StimTrack si un identifiant est présent
  • Cache local (TTL 5 minutes)
  • Bouton de refresh manuel

Exemple

Fix login bug #12345

➡ devient :

https://stimtrack.stimdata.tools/bugs_voir.php?bug_id=12345

2. StimTrack Enhancer

Fichier : TamperMonkey-StimData-StimTrack.js

Améliore l’interface StimTrack.

Fonctionnalités

  • Correction UI (select width, flex layout)
  • Navigation rapide par Track ID
  • Injection du contexte ticket
  • Bouton “Copier Markdown”
  • Export structuré du ticket

Export Markdown

Génère automatiquement :

  • Informations du track
  • Données développement
  • Pièces jointes

Authentification GitHub (IMPORTANT)

Le script GitHub nécessite un Personal Access Token (PAT).

Création du token

  1. Aller sur : https://github.com/settings/tokens

  2. Générer un token avec :

    • repo
  3. Copier dans le script :

const TOKEN = 'YOUR_TOKEN';

Sécurité

  • Le token reste local (Tampermonkey)
  • Ne jamais commit / partager publiquement
  • Régénérer en cas de doute

Objectif

Ces scripts ont pour but de :

  • Réduire les changements de contexte
  • Centraliser les informations dev
  • Accélérer le suivi PR / tickets
  • Améliorer l’UX des outils internes STIMDATA

Notes techniques

  • Aucun backend
  • API GitHub REST
  • localStorage (cache)
  • MutationObserver (StimTrack)
// ==UserScript==
// @name GitHub STIMDATA PR Dashboard
// @namespace https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855
// @version 1.5.1
// @description STIMDATA internal PR dashboard (with caching + Stimtrack integration)
// @author Stanislas Poisson
// @license MIT
// @match https://github.com/STIMDATA*
// @updateURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-GITHUB.js
// @downloadURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-GITHUB.js
// @homepageURL https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855
// @supportURL https://www.patreon.com/c/zairakai
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @run-at document-end
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
/**
* GitHub STIMDATA PR Dashboard
* ================
* Author: Stanislas Poisson
*
* Features:
* - Display all open PRs where you are involved (author/reviewer)
* - Grouped by repository
* - Cache system (localStorage, 5 minutes TTL)
* - Avoid duplicate PRs
* - Detect external Track ID (#12345 at end of title)
* - Add direct link to Stimtrack
*
* UI:
* - Injected under GitHub header
* - Lightweight table display
* - Manual refresh button
*
* Support:
* - Patreon: https://www.patreon.com/c/zairakai
* - Twitch: https://www.twitch.tv/zairakai
*/
(function () {
'use strict';
const TOKEN_KEY = 'github_token';
const ORG = 'STIMDATA';
const CACHE_KEY = 'gh_pr_dashboard_cache_v1';
const CACHE_TTL = 5 * 60 * 1000;
let TOKEN = GM_getValue(TOKEN_KEY, '');
if (! TOKEN) {
TOKEN = prompt('GitHub token required');
GM_setValue(TOKEN_KEY, TOKEN);
}
let currentUser = null;
const uniquePRs = new Map();
/**
* UTILS
* ----------------
*/
/**
* Extract trailing #ID from PR title
* Example: "fix something #12345" → 12345
*/
function extractExternalId(title) {
const match = title.match(/#(\d+)\s*$/);
return match
? match[1]
: null;
}
/**
* Build external link (à adapter si besoin)
*/
function buildExternalLink(id) {
return `https://stimtrack.stimdata.tools/bugs_voir.php?bug_id=${id}`;
}
/**
* CACHE
* ----------------
*/
function saveCache(data) {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({
timestamp: Date.now(),
data
})
);
}
function loadCache() {
const raw = localStorage.getItem(CACHE_KEY);
if (! raw) {
return null;
}
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function isCacheValid(cache) {
return cache
&& (Date.now() - cache.timestamp < CACHE_TTL);
}
function timeAgo(ts) {
const diff = Math.floor((Date.now() - ts) / 1000);
if (diff < 60) {
return `${diff}s ago`;
}
if (diff < 3600) {
return `${Math.floor(diff / 60)}m ago`;
}
return `${Math.floor(diff / 3600)}h ago`;
}
/**
* UI
* ----------------
*/
function getContainer() {
const header = document.querySelector('header[role="banner"]');
if (! header) {
return null;
}
let el = document.getElementById('tm-dashboard');
if (!el) {
el = document.createElement('div');
el.id = 'tm-dashboard';
el.style.cssText = `
margin: var(--base-size-16) auto 0;
padding: 16px;
border: var(--borderWidth-thin,.0625rem) solid var(--borderColor-default);
border-radius: 6px;
background-color: var(--bgColor-accent-muted);
max-width: 1232px;
`;
header.insertAdjacentElement('afterend', el);
}
return el;
}
function setLoading(text) {
const el = getContainer();
if (!el) {
return;
}
el.innerHTML = `
<div style="display:flex;gap:10px;align-items:center;">
<span style="width:10px;height:10px;border-radius:50%;background:#58a6ff;animation:pulse 1s infinite;"></span>
<strong>Loading:</strong> ${text}
</div>
<style>
@keyframes pulse {
0% { opacity:0.3; transform:scale(1); }
50% { opacity:1; transform:scale(1.4); }
100% { opacity:0.3; transform:scale(1); }
}
</style>
`;
}
/**
* API
* ----------------
*/
async function ghFetch(url) {
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${TOKEN}`,
Accept: 'application/vnd.github+json'
}
});
if (! res.ok) {
throw new Error(`GitHub API ${res.status}`);
}
return res.json();
}
async function fetchUser() {
setLoading('User...');
const data = await ghFetch('https://api.github.com/user');
currentUser = data.login;
}
async function fetchPRs() {
setLoading('PRs...');
let page = 1;
while (true) {
const data = await ghFetch(
`https://api.github.com/search/issues?q=org:${ORG}+is:pr+is:open+involves:${currentUser}&per_page=100&page=${page}`
);
const items = data.items || [];
if (! items.length) {
break;
}
for (const pr of items) {
const repoFull = pr.repository_url
.replace('https://api.github.com/repos/', '');
const key = `${repoFull}#${pr.number}`;
if (uniquePRs.has(key)) {
continue;
}
const externalId = extractExternalId(pr.title);
uniquePRs.set(key, {
repo: repoFull.split('/')[1],
number: pr.number,
title: pr.title,
url: pr.html_url,
draft: pr.draft || false,
externalId
});
}
page++;
}
}
/**
* GROUP
* ----------------
*/
function groupByRepo() {
const map = {};
for (const pr of uniquePRs.values()) {
if (! map[pr.repo]) {
map[pr.repo] = [];
}
map[pr.repo].push(pr);
}
return map;
}
/**
* RENDER
* ----------------
*/
function render(grouped, timestamp = null) {
const el = getContainer();
if (! el) {
return;
}
let html = `
<div style="display:flex;justify-content:space-between;align-items:center;">
<h2>PR Dashboard</h2>
<div>
${timestamp ? `<span style="opacity:.6;">Updated ${timeAgo(timestamp)}</span>` : ''}
<button id="tm-refresh" class="btn" style="margin-left:10px;">Refresh</button>
</div>
</div>
`;
const repos = Object.keys(grouped);
if (! repos.length) {
el.innerHTML = `<p>No PR found.</p>`;
return;
}
for (const repo of repos) {
html += `<h3>${repo}</h3>`;
html += `<table style="width:100%;">`;
grouped[repo].forEach(pr => {
html += `
<tr>
<td>
<a href="${pr.url}" target="_blank">
#${pr.number} ${pr.title}
</a>
</td>
<td style="width:100px;">${pr.draft ? 'Draft' : 'Ready'}</td>
<td style="width:120px;">
${pr.externalId
? `<a href="${buildExternalLink(pr.externalId)}" target="_blank">Track #${pr.externalId}</a>`
: ''
}
</td>
</tr>
`;
});
html += `</table>`;
}
el.innerHTML = html;
document.getElementById('tm-refresh')
?.addEventListener('click', () => {
localStorage.removeItem(CACHE_KEY);
location.reload();
});
}
/**
* MAIN
* ----------------
*/
async function init() {
const cache = loadCache();
if (isCacheValid(cache)) {
render(cache.data, cache.timestamp);
return;
}
try {
await fetchUser();
await fetchPRs();
const grouped = groupByRepo();
saveCache(grouped);
render(grouped, Date.now());
} catch (e) {
const el = getContainer();
if (el) {
el.innerHTML = `<p style="color:red;">${e.message}</p>`;
}
console.error(e);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// ==UserScript==
// @name Stimtrack Enhancer
// @namespace https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855
// @version 1.6.0
// @description Track title, quick navigation, markdown export + criticity indicator for Stimtrack
// @author Stanislas Poisson
// @license MIT
// @match https://stimtrack.stimdata.tools/*
// @updateURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-StimTrack.js
// @downloadURL https://gist.githubusercontent.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855/raw/TamperMonkey-StimData-StimTrack.js
// @homepageURL https://gist.github.com/Stanislas-Poisson/ea033ad287fb90cf3dc3cdc4741dc855
// @supportURL https://www.patreon.com/c/zairakai
// @icon https://www.google.com/s2/favicons?sz=64&domain=stimdata.tools
// @run-at document-end
// @grant none
// ==/UserScript==
/**
* Stimtrack Enhancer
* ================
* Author: Stanislas Poisson
*
* Features:
* - Custom sticky track header
* - Criticity indicator (color-coded urgency dot + shadow)
* - Type indicator (Bug / Evolution / TODO icon)
* - Markdown export (track + dev sections + attachments)
* - Quick navigation input (direct track ID jump)
* - UI fixes (Bootstrap select width, layout stability)
*
* Support:
* - Patreon: https://www.patreon.com/c/zairakai
* - Twitch: https://www.twitch.tv/zairakai
*/
(function () {
'use strict';
const params = new URLSearchParams(window.location.search);
const bugId = params.get('bug_id');
/**
* CONFIG
*/
const CRITICITY_CONFIG = {
bloquant: { color: '#eb3b3b', label: 'Bloquant' },
majeur: { color: '#f1af3e', label: 'Majeur' },
normal: { color: '#2081c8', label: 'Normal' },
mineur: { color: '#7dbdec', label: 'Mineur' },
trackday: { color: '#bc54bd', label: 'Trackday' }
};
const TYPE_CONFIG = {
bug: { icon: '🐞' },
evolution: { icon: '🚀' },
todo: { icon: '📝' }
};
/**
* GLOBAL STYLE FIX
*/
function injectStyles() {
if (document.getElementById('tm-style-fix')) return;
const style = document.createElement('style');
style.id = 'tm-style-fix';
style.innerHTML = `
.bootstrap-select .filter-option.pull-left { width: auto !important; }
.project-navbar-select { width: auto !important; max-width: none !important; }
.tm-flex-wrapper {
display: flex;
align-items: stretch;
gap: 8px;
width: 100%;
}
#quick-track-input {
height: auto;
align-self: stretch;
width: 80px;
}
#custom-track-header-sticky {
position: sticky;
top: 85px;
z-index: 1040;
display: grid;
grid-template-columns: 1fr auto;
margin: 0 -15px 10px;
box-shadow: 0 .5rem .5rem rgb(0 0 0 / 5%);
border-radius: 0 0 1rem 1rem;
overflow: hidden;
}
.cth-h2 {
width: 100%;
height: 46px;
background-color: #095893;
}
.cth-h2 h2 {
margin: 0;
padding: 0;
line-height: 46px;
font-size: 18px;
color: #fff;
margin-left: 10px;
}
.cth-alert {
height: 46px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f9b229;
color: #fff;
border-left: 3px solid;
padding: 0 10px;
}
.cth-alert-btn {
color: inherit;
text-decoration: none;
display: flex;
align-items: center;
}
.cth-alert-btn .glyphicon { margin-right: 10px; font-size: 18px; }
.cth-alert-btn span { font-size: 18px; }
.cth-track-id,
.cth-markdown {
display: flex;
align-items: center;
background: #f5f5f5;
border-top: 1px solid #e0e0e0;
padding: 6px 10px;
}
.cth-markdown { justify-content: flex-end; }
.cth-track-id span {
font-weight: 600;
color: #555;
font-size: 0.9em;
}
.cth-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
display: inline-block;
}
.cth-type {
margin-right: 6px;
font-size: 14px;
}
`;
document.head.appendChild(style);
}
/**
* HELPERS
*/
function getFieldValue(labelText) {
const groups = document.querySelectorAll('.form-group');
for (const group of groups) {
const label = group.querySelector('label')?.textContent
?.replace(/\u00a0/g, ' ')
?.replace(':', '')
?.trim()
?.toLowerCase();
if (label === labelText.toLowerCase()) {
return group.querySelector('.form-control-static')?.innerText.trim();
}
}
return null;
}
function extractSectionData(section) {
const data = {};
section.querySelectorAll('.form-group').forEach(group => {
const labelEl = group.querySelector('label');
const valueEl = group.querySelector('.form-control-static');
if (!labelEl || !valueEl) return;
const label = labelEl.textContent
.replace(/\u00a0/g, ' ')
.replace(':', '')
.trim();
const value = valueEl.innerText.trim();
if (!value) return;
data[label] = value;
});
return data;
}
function extractAttachments(section) {
const files = [];
section.querySelectorAll('.download-all a[href*="docs/"]').forEach(link => {
const name = link.textContent.trim();
const url = link.href;
if (name) files.push({ name, url });
});
return files;
}
function formatEntry(key, value) {
if (!value.includes('\n')) return `- ${key}: ${value}\n`;
return `- ${key}:\n${value.split('\n').filter(l => l.trim()).map(l => ` ${l.trim()}`).join('\n')}\n`;
}
function buildMarkdown(trackId, trackData, devData, attachments) {
let md = `# Track #${trackId}\n\n`;
md += `## Track\n\n`;
Object.entries(trackData).forEach(([k, v]) => md += formatEntry(k, v));
md += `\n## Développement\n\n`;
Object.entries(devData).forEach(([k, v]) => md += formatEntry(k, v));
if (attachments.length) {
md += `\n## Annexe\n\n`;
attachments.forEach(a => md += `- ${a.name}\n`);
}
return md;
}
/**
* STICKY HEADER
*/
function injectBadge() {
const alertAnchor = document.querySelector('.btn-open-modal[data-form="send-alert"]');
if (!alertAnchor || document.querySelector('#custom-track-header-sticky')) return;
const titleDiv = document.querySelector('.title-track-view-div');
if (!titleDiv) return;
injectStyles();
const criticityRaw = getFieldValue('Criticité');
const typeRaw = getFieldValue('Type de demande');
const criticityKey = criticityRaw?.toLowerCase();
const typeKey = typeRaw?.toLowerCase();
const criticity = CRITICITY_CONFIG[criticityKey] ?? { color: '#999', label: criticityRaw ?? 'N/A' };
const type = TYPE_CONFIG[typeKey] ?? { icon: '❓' };
const h2Text = titleDiv.querySelector('h2')?.textContent.trim() ?? '';
const sticky = document.createElement('div');
sticky.id = 'custom-track-header-sticky';
sticky.innerHTML = `
<div class="cth-h2"><h2>${h2Text}</h2></div>
<div class="cth-alert">
<a href="#" class="cth-alert-btn">
<i class="glyphicon glyphicon-warning-sign"></i><span>Alerter</span>
</a>
</div>
<div class="cth-track-id">
<span class="cth-dot"></span>
<span class="cth-type">${type.icon}</span>
<span>Track #${bugId}</span>
</div>
<div class="cth-markdown">
<button id="copy-md-btn" class="btn btn-default btn-sm">
<i class="fa fa-copy"></i> Copier Markdown
</button>
</div>
`;
const container = document.querySelector('.container');
if (container) container.insertBefore(sticky, container.firstChild);
titleDiv.style.display = 'none';
alertAnchor.style.display = 'none';
sticky.querySelector('.cth-dot').style.backgroundColor = criticity.color;
sticky.style.boxShadow = `0 .5rem .5rem ${criticity.color}55`;
sticky.querySelector('.cth-alert-btn').addEventListener('click', e => {
e.preventDefault();
alertAnchor.click();
});
const mdBtn = sticky.querySelector('#copy-md-btn');
mdBtn.addEventListener('click', () => {
const trackSection = document.querySelector('#form_opea');
const devSection = document.querySelector('#form_dev');
if (!trackSection || !devSection) return;
const md = buildMarkdown(
bugId,
extractSectionData(trackSection),
extractSectionData(devSection),
extractAttachments(trackSection)
);
navigator.clipboard.writeText(md);
mdBtn.innerHTML = '<i class="fa fa-check"></i> Copié !';
setTimeout(() => {
mdBtn.innerHTML = '<i class="fa fa-copy"></i> Copier Markdown';
}, 1500);
});
}
/**
* QUICK NAV
*/
function injectQuickNav() {
const nav = document.querySelector('.project-navbar-select');
if (!nav || document.querySelector('#quick-track-input')) return;
injectStyles();
const wrapper = document.createElement('div');
wrapper.className = 'tm-flex-wrapper';
const input = document.createElement('input');
input.id = 'quick-track-input';
input.type = 'number';
input.placeholder = 'Track ID';
input.className = 'form-control input-sm';
if (bugId) input.value = bugId;
input.addEventListener('keydown', e => {
if (e.key !== 'Enter') return;
const id = input.value.trim();
if (!id) return;
window.location.href = `https://stimtrack.stimdata.tools/bugs_voir.php?bug_id=${id}`;
});
while (nav.firstChild) wrapper.appendChild(nav.firstChild);
wrapper.insertBefore(input, wrapper.firstChild);
nav.appendChild(wrapper);
}
function init() {
if (bugId) injectBadge();
injectQuickNav();
}
const observer = new MutationObserver(init);
observer.observe(document.body, { childList: true, subtree: true });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment