Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,6 @@ generated-days/
# Node dependencies
node_modules/
.claude/settings.local.json

# Superpowers (AI agent specs/plans - not for version control)
docs/superpowers/
1 change: 0 additions & 1 deletion installer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ services:
GRAFANA_INTERNAL_URL: "http://grafana:3000"
GRAFANA_EXTERNAL_URL: "https://grafana.westernformularacing.org"
GRAFANA_API_TOKEN: "${GRAFANA_API_TOKEN}"
GRAFANA_DATASOURCE_UID: "influxdb-wfr-v2"
GRAFANA_FOLDER_UID: "${GRAFANA_FOLDER_UID:-}"
CORS_ORIGIN: "http://localhost:3000,https://pecan.westernformularacing.org,https://pecan-dev.westernformularacing.org"
INFLUX_TABLE: "${INFLUX_DATABASE:-WFR26}"
Expand Down
88 changes: 69 additions & 19 deletions installer/grafana-bridge/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,51 +31,72 @@ const GRAFANA_EXTERNAL_URL =
"https://grafana.westernformularacing.org";
const GRAFANA_API_TOKEN = process.env.GRAFANA_API_TOKEN;
const GRAFANA_FOLDER_UID = process.env.GRAFANA_FOLDER_UID || "";
const DATASOURCE_UID = process.env.GRAFANA_DATASOURCE_UID || "influxdb-wfr-v2";

// Only allow CAN signal name characters (alphanumeric, underscore, hyphen, dot)
const SIGNAL_NAME_RE = /^[A-Za-z0-9_.\-]+$/;

// Regex to extract season name from InfluxDB datasource database field (e.g., "WFR26" from "WFR26@iox")
const SEASON_RE = /^(WFR\d+)/;

// Fetch all InfluxDB datasources from Grafana and find the one matching the given season
async function findDatasourceUidForSeason(season) {
const response = await fetch(`${GRAFANA_INTERNAL_URL}/api/datasources`, {
headers: { Authorization: `Bearer ${GRAFANA_API_TOKEN}` },
});
if (!response.ok) {
throw new Error(`Grafana datasources API error: ${response.status}`);
}
const datasources = await response.json();
for (const ds of datasources) {
if (ds.type === "influxdb" && ds.name) {
const match = SEASON_RE.exec(ds.name);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match season against datasource metadata, not display name

The new resolver parses ds.name with SEASON_RE, but in this repo the provisioned datasource is named InfluxDB-WFR (see installer/grafana/provisioning/datasources/influxdb.yml) and does not start with WFR##. In that common deployment, findDatasourceUidForSeason never finds a match and /api/grafana/create-dashboard now returns 500 for every request instead of creating dashboards. This regression was introduced when the static GRAFANA_DATASOURCE_UID path was removed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haoruizhou wanna look at this

if (match && match[1] === season) {
return ds.uid;
}
}
}
throw new Error(`No InfluxDB datasource found for season: ${season}`);
}

function validateSignalName(name) {
if (typeof name !== "string" || name.length === 0 || name.length > 128) {
return false;
}
return SIGNAL_NAME_RE.test(name);
}

const INFLUX_TABLE = process.env.INFLUX_TABLE || "WFR26";

function buildQuery(signalName) {
function buildQuery(signalName, season) {
return [
"SELECT",
' DATE_BIN(INTERVAL \'100 milliseconds\', t."time", TIMESTAMP \'1970-01-01 00:00:00\') AS "time",',
` AVG(t."${signalName}") AS "value"`,
' DATE_BIN(INTERVAL \'100 milliseconds\', "time", TIMESTAMP \'1970-01-01 00:00:00\') AS "time",',
` AVG("${signalName}") AS "${signalName}"`,
"FROM",
` "iox"."${INFLUX_TABLE}" AS t`,
` "iox"."${season}"`,
"WHERE",
' t."time" >= $__timeFrom()',
' AND t."time" <= $__timeTo()',
' "time" >= $__timeFrom()',
' AND "time" <= $__timeTo()',
"GROUP BY",
' 1',
' DATE_BIN(INTERVAL \'100 milliseconds\', "time", TIMESTAMP \'1970-01-01 00:00:00\')',
"ORDER BY",
' "time" ASC',
].join("\n");
}

function buildPanel(signalName, index) {
function buildPanel(signalName, index, dsUid, season) {
return {
type: "timeseries",
title: signalName,
datasource: {
type: "influxdb",
uid: DATASOURCE_UID,
},
targets: [
{
refId: "A",
query: buildQuery(signalName),
dataset: "iox",
datasource: {
type: "influxdb",
uid: dsUid,
},
rawQuery: true,
resultFormat: "time_series",
rawSql: buildQuery(signalName, season),
format: "time_series",
},
],
gridPos: {
Expand Down Expand Up @@ -140,9 +161,19 @@ router.post("/api/grafana/create-dashboard", async (req, res) => {

const uid = "pecan_" + crypto.randomBytes(4).toString("hex");
const now = new Date();
const currentSeason = `WFR${now.getFullYear() % 100}`;
const title = `PECAN Analysis - ${now.toISOString().replace("T", " ").substring(0, 16)}`;

const panels = signalNames.map((name, i) => buildPanel(name, i));
// Fetch the real datasource UID for the current season
let dsUid;
try {
dsUid = await findDatasourceUidForSeason(currentSeason);
} catch (err) {
console.error("Failed to find datasource UID:", err.message);
return res.status(500).json({ error: `Failed to resolve datasource for ${currentSeason}: ${err.message}` });
}

const panels = signalNames.map((name, i) => buildPanel(name, i, dsUid, currentSeason));

const payload = {
dashboard: {
Expand All @@ -154,7 +185,26 @@ router.post("/api/grafana/create-dashboard", async (req, res) => {
schemaVersion: 39,
version: 0,
panels,
time: { from: "now-1h", to: "now" },
time: { from: "now-24h", to: "now" },
variables: {
list: [
{
kind: "DatasourceVariable",
spec: {
name: "year",
label: "Year",
current: { text: currentSeason, value: dsUid },
hide: "",
multi: false,
includeAll: false,
pluginId: "influxdb",
regex: "WFR2[0-9]+",
skipUrlSync: false,
refresh: 1,
},
},
],
},
},
overwrite: false,
};
Expand Down
Loading