Compare commits

...
Author SHA1 Message Date
JalleliAchraf 658f524eff glitch fix attempt 2026-02-18 10:07:43 +01:00
JalleliAchraf 88b0c87a51 Merge branch '1004-target-line-with-current-target' of github.com:qitechgmbh/control into 1004-target-line-with-current-target 2026-02-18 10:02:43 +01:00
JalleliAchraf 4178c90769 formatting 2026-02-17 20:49:28 +01:00
JalleliAchraf 2c99231f1f target line jittering replaced with a natural flow motion 2026-02-17 20:49:28 +01:00
Saurabh Bagade 26b417c0ec refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-02-17 20:49:28 +01:00
Saurabh Bagade 7df303bdf4 code formatting fixed for animation.ts 2026-02-17 20:49:28 +01:00
Saurabh Bagade 93c5e8d599 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade c5fbdd0a0f refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-02-17 20:49:28 +01:00
Saurabh Bagade 45e03b9c93 fixed the issue with the target line 2026-02-17 20:49:28 +01:00
Christian 5562cf5a53 Add GitHub Actions workflow for issue status checks
Refactor issue status check workflow to identify stale assigned issues and request updates

Refactor linked pull request check to use search API for improved reliability

Add comprehensive error handling and rate limit monitoring
2026-02-17 17:48:51 +01:00
Achraf Jalleli 3a24c24836 Merge branch 'master' into 1004-target-line-with-current-target 2026-02-13 20:04:49 +01:00
JalleliAchraf 26e4c6ab6c formatting 2026-02-13 19:58:25 +01:00
JalleliAchraf 60f95ec9b6 target line jittering replaced with a natural flow motion 2026-02-13 19:53:25 +01:00
Saurabh Bagade 2c7f65dc78 refactored to reduce slanted/vertical wiggle and remove horizontal wiggle 2026-01-31 13:54:14 +01:00
Saurabh Bagade d3d2ab4828 code formatting fixed for animation.ts 2026-01-31 13:54:14 +01:00
Saurabh Bagade a839a374a3 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 6049737955 refactored update of target lines only when the state changes for better performance. removed code duplication in extruder2 and extruder3 2026-01-31 13:54:14 +01:00
Saurabh Bagade 0c4a1eec39 fixed the issue with the target line 2026-01-31 13:54:14 +01:00
19 changed files with 782 additions and 30 deletions

No files matched your search

+213
View File
@@ -0,0 +1,213 @@
name: Stale Assigned Issue Check
on:
schedule:
# Run every Sunday at 9:00 AM UTC
- cron: '0 9 * * 0'
workflow_dispatch: # Allow manual triggering for testing
jobs:
check-stale-issues:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: read
steps:
- name: Check assigned issues for status updates
uses: actions/github-script@v7
with:
script: |
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
const now = new Date();
// Helper function to check rate limits
async function checkRateLimit() {
try {
const { data: rateLimit } = await github.rest.rateLimit.get();
const remaining = rateLimit.resources.core.remaining;
const reset = new Date(rateLimit.resources.core.reset * 1000);
console.log(`Rate limit: ${remaining} requests remaining (resets at ${reset.toISOString()})`);
return remaining > 10; // Stop if fewer than 10 requests remaining
} catch (error) {
console.log(`Could not check rate limit: ${error.message}`);
return true; // Continue on error
}
}
// Get all open issues
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
console.log(`Found ${issues.length} open issues`);
// Helper function to check if issue has linked PRs using GraphQL
async function hasLinkedPRs(issueNumber) {
try {
const query = `
query($owner: String!, $repo: String!, $issueNumber: Int!) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
timelineItems(first: 100, itemTypes: CONNECTED_EVENT) {
nodes {
... on ConnectedEvent {
subject {
... on PullRequest {
number
state
}
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: issueNumber
});
const linkedPRs = result.repository.issue.timelineItems.nodes
.filter(node => node.subject && node.subject.state === 'OPEN')
.map(node => node.subject.number);
return linkedPRs.length > 0;
} catch (error) {
console.log(`Could not check linked PRs for issue #${issueNumber}: ${error.message}`);
return false; // Assume no linked PRs on error to avoid false positives
}
}
let processedCount = 0;
let commentedCount = 0;
let errorCount = 0;
for (const issue of issues) {
try {
// Check rate limit periodically
if (processedCount % 10 === 0 && !(await checkRateLimit())) {
console.log('Rate limit nearly exhausted, stopping to avoid hitting limit');
break;
}
// Skip pull requests (they appear in issues list too)
if (issue.pull_request) {
continue;
}
// Skip if not assigned
if (!issue.assignees || issue.assignees.length === 0) {
console.log(`Issue #${issue.number}: Not assigned, skipping`);
continue;
}
console.log(`Checking issue #${issue.number}: ${issue.title}`);
processedCount++;
// Check if issue has linked PRs
const hasOpenLinkedPR = await hasLinkedPRs(issue.number);
if (hasOpenLinkedPR) {
console.log(`Issue #${issue.number}: Has linked open PR, skipping`);
continue;
}
// Get all comments on the issue
let comments = [];
try {
comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100
});
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not fetch comments - ${commentError.message}`);
errorCount++;
continue;
}
// Find the most recent comment or update
let lastActivity = new Date(issue.created_at);
if (issue.updated_at) {
lastActivity = new Date(issue.updated_at);
}
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
const lastCommentDate = new Date(lastComment.created_at);
if (lastCommentDate > lastActivity) {
lastActivity = lastCommentDate;
}
}
// Check if last activity was more than a week ago
const daysSinceActivity = Math.floor((now - lastActivity) / (1000 * 60 * 60 * 24));
if (daysSinceActivity >= 7) {
console.log(`Issue #${issue.number}: No activity for ${daysSinceActivity} days`);
// Check if we already posted a status request in the last 7 days
const recentComments = comments.filter(comment => {
const commentDate = new Date(comment.created_at);
return (now - commentDate) < ONE_WEEK_MS;
});
const alreadyAsked = recentComments.some(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('status update')
);
if (alreadyAsked) {
console.log(`Issue #${issue.number}: Already requested status this week, skipping`);
continue;
}
// Post a status request comment
try {
const assigneeNames = issue.assignees.map(a => `@${a.login}`).join(', ');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Hi ${assigneeNames}! 👋\n\n` +
`This issue has been assigned to you for ${daysSinceActivity} days with no recent activity or linked pull requests.\n\n` +
`Could you please provide a status update? If you're:\n` +
`- ✅ Working on it: Let us know your progress\n` +
`- 🔄 Need help: Tag someone or describe what's blocking you\n` +
`- 🚫 No longer working on it: Let us know so we can reassign\n\n` +
`Thank you!`
});
console.log(`Issue #${issue.number}: Posted status request comment`);
commentedCount++;
} catch (commentError) {
console.error(`Issue #${issue.number}: Could not post comment - ${commentError.message}`);
errorCount++;
// Continue to next issue
}
} else {
console.log(`Issue #${issue.number}: Activity ${daysSinceActivity} days ago, still fresh`);
}
} catch (error) {
console.error(`Issue #${issue.number}: Error processing - ${error.message}`);
errorCount++;
// Continue to next issue instead of failing the entire workflow
continue;
}
}
console.log(`\nStale issue check complete!`);
console.log(`Processed: ${processedCount} issues`);
console.log(`Commented on: ${commentedCount} issues`);
console.log(`Errors: ${errorCount}`);
+77 -4
View File
@@ -5,8 +5,23 @@ import {
AnimationRefs,
AnimationState,
BigGraphProps,
GraphLine,
SeriesData,
} from "./types";
import { alignTargetSeriesToTimestamps } from "@/lib/timeseries";
function hasHistoricalDashedTargets(
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>,
): boolean {
return (
lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
export function useAnimationRefs(): AnimationRefs {
const animationFrameRef = useRef<number | null>(null);
@@ -24,12 +39,14 @@ export function useAnimationRefs(): AnimationRefs {
values: number[];
}>({ timestamps: [], values: [] });
const realPointsCountRef = useRef(0);
const targetLineCacheRef = useRef<Map<number, number[]>>(new Map());
return {
animationFrame: animationFrameRef,
animationState: animationStateRef,
lastRenderedData: lastRenderedDataRef,
realPointsCount: realPointsCountRef,
targetLineCache: targetLineCacheRef,
};
}
@@ -50,8 +67,12 @@ export function buildUPlotData(
values: number[],
realPointsCount: number | undefined,
realPointsCountRef: React.RefObject<number>,
config: { lines?: Array<{ show?: boolean; value: number }> },
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
allSeriesData?: number[][],
targetLineCache?: React.RefObject<Map<number, number[]>>,
freezeTargetTail: boolean = false,
): uPlot.AlignedData {
const uData: uPlot.AlignedData = [timestamps];
@@ -70,9 +91,55 @@ export function buildUPlotData(
}
// Add config lines
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
uData.push(timestamps.map(() => line.value));
let lineData: number[];
if (line.targetSeries && targetLineCache) {
// Target line with history - use cache to prevent wiggle
const cached = targetLineCache.current.get(lineIndex);
if (cached && cached.length <= timestamps.length) {
// Reuse cached data for existing points, extend with current value
const extension = timestamps.length - cached.length;
if (extension > 0) {
const stableTailValue = cached[cached.length - 1] ?? line.value;
lineData = [
...cached,
...new Array(extension).fill(
freezeTargetTail ? stableTailValue : line.value,
),
];
} else {
lineData = [...cached];
}
if (!freezeTargetTail) {
targetLineCache.current.set(lineIndex, lineData);
}
} else {
// Full recalculation (first time or chart was rebuilt)
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
targetLineCache.current.set(lineIndex, lineData);
}
} else if (line.targetSeries) {
// targetSeries but no cache available - full calculation
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
} else {
// Constant value line (original behavior)
lineData = timestamps.map(() => line.value);
}
uData.push(lineData);
lineIndex++;
}
});
@@ -88,8 +155,10 @@ export function animateNewPoint(
viewMode: string,
selectedTimeWindow: number | "all",
startTimeRef: React.RefObject<number | null>,
config: { lines?: Array<{ show?: boolean; value: number }> },
updateYAxisScale: (xMin?: number, xMax?: number) => void, // Updated signature
config: {
lines?: Array<Partial<GraphLine> & { show?: boolean; value: number }>;
},
updateYAxisScale: (xMin?: number, xMax?: number) => void,
getAllSeriesData?: () => number[][],
): void {
if (targetData.timestamps.length <= currentData.timestamps.length) {
@@ -123,6 +192,8 @@ export function animateNewPoint(
targetIndex: newIndex,
};
const freezeTargetTail = hasHistoricalDashedTargets(config.lines);
const animate = (currentTime: number) => {
if (
!uplotRef.current ||
@@ -173,6 +244,8 @@ export function animateNewPoint(
refs.realPointsCount,
config,
allSeriesData,
refs.targetLineCache,
freezeTargetTail,
);
uplotRef.current.setData(animatedUData);
+130 -5
View File
@@ -1,5 +1,9 @@
import uPlot from "uplot";
import { seriesToUPlotData, TimeSeries } from "@/lib/timeseries";
import {
seriesToUPlotData,
TimeSeries,
alignTargetSeriesToTimestamps,
} from "@/lib/timeseries";
import {
createEventHandlers,
attachEventHandlers,
@@ -8,6 +12,13 @@ import {
import { BigGraphProps, CreateChartParams } from "./types";
import { normalizeDataSeries } from "./animation";
type CustomDashedTargetLine = {
dataIndex: number;
dash: number[];
color: string;
width: number;
};
export function createChart({
containerRef,
uplotRef,
@@ -56,11 +67,28 @@ export function createChart({
uPlotData.push(values as any);
});
// Add config lines data
// Add config lines data and populate target line cache
animationRefs.targetLineCache.current.clear();
let lineIndex = 0;
config.lines?.forEach((line) => {
if (line.show !== false) {
const lineData = new Array(timestamps.length).fill(line.value);
let lineData: number[];
if (line.targetSeries) {
// Use historical target values aligned with data timestamps
lineData = alignTargetSeriesToTimestamps(
line.targetSeries,
timestamps,
line.value,
);
// Cache the computed target line data so subsequent updates
// can extend it instead of recalculating (prevents wiggle)
animationRefs.targetLineCache.current.set(lineIndex, [...lineData]);
} else {
// Use constant value (original behavior)
lineData = new Array(timestamps.length).fill(line.value);
}
uPlotData.push(lineData as any);
lineIndex++;
}
});
@@ -142,6 +170,7 @@ export function createChart({
// Build series configuration for ALL series (but control visibility)
const seriesConfig: uPlot.Series[] = [{ label: "Time" }];
const customDashedTargetLines: CustomDashedTargetLine[] = [];
// Add ALL data series with visibility control
const defaultColors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"];
@@ -167,16 +196,35 @@ export function createChart({
});
// Add config lines
let visibleLineIndex = 0;
const firstConfigLineDataIndex = 1 + allOriginalSeries.length;
config.lines?.forEach((line) => {
if (line.show !== false) {
const dash =
line.dash ?? (line.type === "threshold" ? [5, 5] : undefined);
const lineDataIndex = firstConfigLineDataIndex + visibleLineIndex;
const isHistoricalDashedTarget = !!line.targetSeries && !!dash?.length;
if (isHistoricalDashedTarget) {
customDashedTargetLines.push({
dataIndex: lineDataIndex,
dash: dash!,
color: line.color,
width: line.width ?? 1,
});
}
seriesConfig.push({
label: line.label,
stroke: line.color,
// Historical dashed targets are custom rendered in draw hook to avoid
// canvas dash shimmer during smooth live updates.
stroke: isHistoricalDashedTarget ? "rgba(0,0,0,0)" : line.color,
width: line.width ?? 1,
dash: line.dash ?? (line.type === "threshold" ? [5, 5] : undefined),
dash: isHistoricalDashedTarget ? undefined : dash,
show: true,
points: { show: false },
});
visibleLineIndex++;
}
});
@@ -306,6 +354,83 @@ export function createChart({
}
},
],
draw: [
(u) => {
if (customDashedTargetLines.length === 0) return;
const xData = u.data[0] as number[] | undefined;
if (!xData || xData.length === 0) return;
const xMin = u.scales.x?.min;
const xMax = u.scales.x?.max;
if (xMin === undefined || xMax === undefined) return;
const pxPerX = u.bbox.width / Math.max(xMax - xMin, 1e-9);
const ctx = u.ctx;
const left = u.bbox.left;
const top = u.bbox.top;
const right = left + u.bbox.width;
const bottom = top + u.bbox.height;
ctx.save();
ctx.beginPath();
ctx.rect(left, top, u.bbox.width, u.bbox.height);
ctx.clip();
ctx.lineCap = "butt";
customDashedTargetLines.forEach(
({ dataIndex, dash, color, width }) => {
const yData = u.data[dataIndex] as
| Array<number | null>
| undefined;
if (!yData || yData.length === 0) return;
const dashPeriod = dash.reduce((acc, curr) => acc + curr, 0);
ctx.setLineDash(dash);
// Anchor dash phase to world-X so dashes remain stable while axis slides.
ctx.lineDashOffset =
dashPeriod > 0 ? -((xMin * pxPerX) % dashPeriod) : 0;
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.beginPath();
let started = false;
let prevX = 0;
let prevY = 0;
for (let i = 0; i < xData.length; i++) {
const value = yData[i];
if (value === null || value === undefined) continue;
const x = u.valToPos(xData[i], "x", true);
const y = u.valToPos(value, "y", true);
if (x < left || x > right || y < top || y > bottom) continue;
if (!started) {
ctx.moveTo(x, y);
started = true;
prevX = x;
prevY = y;
continue;
}
if (x !== prevX) {
ctx.lineTo(x, prevY);
}
if (y !== prevY) {
ctx.lineTo(x, y);
}
prevX = x;
prevY = y;
}
if (started) {
ctx.stroke();
}
},
);
ctx.restore();
},
],
},
scales: {
x: {
+25 -3
View File
@@ -14,6 +14,17 @@ import {
AnimationRefs,
} from "./types";
function hasHistoricalDashedTargets(config: GraphConfig): boolean {
return (
config.lines?.some(
(line) =>
line.show !== false &&
!!line.targetSeries &&
(line.dash?.length ?? 0) > 0,
) ?? false
);
}
// Retrieve all series data as an array of number arrays
function getAllSeriesData(data: BigGraphProps["newData"]): number[][] {
const normalized = normalizeDataSeries(data);
@@ -77,10 +88,16 @@ export function useLiveMode({
return;
try {
const hasDashedHistoricalTargets = hasHistoricalDashedTargets(config);
const [timestamps, values] = seriesToUPlotData(primaryData.long);
const cur = primaryData.current;
const liveTimestamps = [...timestamps, cur.timestamp];
const liveValues = [...values, cur.value];
const lastHistoricalTimestamp =
timestamps[timestamps.length - 1] ?? cur.timestamp;
const shouldAppendCurrent = cur.timestamp > lastHistoricalTimestamp;
const liveTimestamps = shouldAppendCurrent
? [...timestamps, cur.timestamp]
: timestamps;
const liveValues = shouldAppendCurrent ? [...values, cur.value] : values;
const allSeriesValues = getAllSeriesData(newData);
const normalized = normalizeDataSeries(newData);
@@ -96,7 +113,9 @@ export function useLiveMode({
(seriesValues.length > 0
? seriesValues[seriesValues.length - 1]
: 0);
return [...seriesValues, currentValue];
return shouldAppendCurrent
? [...seriesValues, currentValue]
: seriesValues;
})
: [];
@@ -107,6 +126,8 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
hasDashedHistoricalTargets,
);
uplotRef.current.setData(liveData);
@@ -283,6 +304,7 @@ export function useLiveMode({
animationRefs.realPointsCount,
config,
additionalSeriesValues,
animationRefs.targetLineCache,
);
uplotRef.current.setData(uData);
+4
View File
@@ -26,7 +26,10 @@ export type PropGraphSync = {
// Configuration types for additional lines
export type GraphLine = {
type: "threshold" | "target";
/** Current value (used as fallback and for Y-axis scaling) */
value: number;
/** Optional TimeSeries for historical target values. When provided, the line shows the target history over time instead of a constant horizontal line */
targetSeries?: TimeSeries;
color: string;
label?: string;
width?: number;
@@ -158,6 +161,7 @@ export interface AnimationRefs {
values: number[];
}>;
realPointsCount: React.RefObject<number>;
targetLineCache: React.RefObject<Map<number, number[]>>;
}
export interface HandlerRefs {
@@ -4,7 +4,11 @@ import uPlot from "uplot";
import { seriesToUPlotData } from "@/lib/timeseries";
import { BigGraphProps, SeriesData, AnimationRefs, HandlerRefs } from "./types";
import { GraphExportData } from "./excelExport";
import { getPrimarySeries, stopAnimations } from "./animation";
import {
getPrimarySeries,
stopAnimations,
normalizeDataSeries,
} from "./animation";
import { createChart } from "./createChart";
import { LiveModeHandlers } from "./liveMode";
import { HistoricalModeHandlers } from "./historicalMode";
@@ -106,6 +110,10 @@ export function useBigGraphEffects({
handleTimeWindowChangeInternal,
}: UseBigGraphEffectsProps) {
const [isChartCreated, setIsChartCreated] = useState(false);
const setChartCreatedState = (created: boolean) => {
setIsChartCreated(created);
chartCreatedRef.current = created;
};
// Track the last synchronized state for comparison
const lastSyncStateRef = useRef({
@@ -239,18 +247,25 @@ export function useBigGraphEffects({
updateYAxisScale,
]);
// Create and initialize the chart when data becomes available
// Compute stable structural keys for chart creation.
// These only change when series count or visible line count changes,
// NOT on every data update (which would destroy/recreate the chart every ~1s).
const seriesWithDataCount = normalizeDataSeries(newData).filter(
(s) => s.newData !== null,
).length;
const visibleLineCount =
config.lines?.filter((l) => l.show !== false)?.length ?? 0;
// Create and initialize the chart when data becomes available or structure changes
useEffect(() => {
if (!containerRef.current || !primarySeries?.newData?.long) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
const [timestamps] = seriesToUPlotData(primarySeries.newData.long);
if (timestamps.length === 0) {
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
return;
}
@@ -280,8 +295,7 @@ export function useBigGraphEffects({
showFromTimestamp,
});
setIsChartCreated(true);
chartCreatedRef.current = true;
setChartCreatedState(true);
if (isLiveMode) {
lastProcessedCountRef.current = timestamps.length;
@@ -292,10 +306,9 @@ export function useBigGraphEffects({
uplotRef.current?.destroy();
uplotRef.current = null;
stopAnimations(animationRefs);
setIsChartCreated(false);
chartCreatedRef.current = false;
setChartCreatedState(false);
};
}, [primarySeries?.newData?.long, containerRef.current]);
}, [seriesWithDataCount, visibleLineCount, containerRef.current]);
// Process new historical data in live mode
useEffect(() => {
+53
View File
@@ -304,3 +304,56 @@ export function resetSeries(series: Series): void {
series.lastTimestamp = 0;
series.validCount = 0;
}
/**
* Aligns target series values with a given set of timestamps.
* For each timestamp in dataTimestamps, finds the most recent target value
* that was set before or at that timestamp (step function interpolation).
*
* @param targetSeries - The TimeSeries containing historical target values
* @param dataTimestamps - Array of timestamps to align the target values to
* @param fallbackValue - Value to use when no target was set before a timestamp
* @returns Array of values aligned with dataTimestamps
*/
export function alignTargetSeriesToTimestamps(
targetSeries: TimeSeries,
dataTimestamps: number[],
fallbackValue: number,
): number[] {
if (dataTimestamps.length === 0) {
return [];
}
// Extract target series data in chronological order
const [targetTimestamps, targetValues] = extractDataFromSeries(
targetSeries.long,
);
if (targetTimestamps.length === 0) {
// No target history, use fallback for all timestamps
return new Array(dataTimestamps.length).fill(fallbackValue);
}
const result: number[] = [];
let targetIndex = 0;
for (const dataTs of dataTimestamps) {
// Find the most recent target value at or before this timestamp
// Advance targetIndex while the next target timestamp is still <= dataTs
while (
targetIndex < targetTimestamps.length - 1 &&
targetTimestamps[targetIndex + 1] <= dataTs
) {
targetIndex++;
}
// If the current target timestamp is > dataTs, we haven't reached the first target yet
if (targetTimestamps[targetIndex] > dataTs) {
result.push(fallbackValue);
} else {
result.push(targetValues[targetIndex]);
}
}
return result;
}
@@ -24,6 +24,13 @@ export function Extruder2GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder2GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder2NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder2MessageHandler(
store: StoreApi<Extruder2NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder2NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder2MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder2MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder2NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder2() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder2Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder2() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -24,6 +24,13 @@ export function Extruder3GraphsPage() {
motorCurrent,
motorPower,
combinedPower,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3();
const syncHook = useGraphSync("extruder-graphs");
@@ -49,6 +56,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.nozzle.target_temperature,
targetSeries: targetNozzleTemperature,
color: "#ef4444",
show: true,
},
@@ -69,6 +77,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.front.target_temperature,
targetSeries: targetFrontTemperature,
color: "#f59e0b",
show: true,
},
@@ -89,6 +98,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.middle.target_temperature,
targetSeries: targetMiddleTemperature,
color: "#8b5cf6",
show: true,
},
@@ -109,6 +119,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.heating_states.back.target_temperature,
targetSeries: targetBackTemperature,
color: "#3b82f6",
show: true,
},
@@ -253,6 +264,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.pressure_state.target_bar,
targetSeries: targetPressure,
color: "#3b82f6",
show: true,
},
@@ -306,6 +318,7 @@ export function Extruder3GraphsPage() {
{
type: "target" as const,
value: state.screw_state.target_rpm,
targetSeries: targetScrewRpm,
color: "#8b5cf6",
show: true,
},
@@ -221,6 +221,14 @@ export type Extruder3NamespaceStore = {
// Combined power consumption and energy
combinedPower: TimeSeries;
totalEnergyKWh: TimeSeries;
// Target value history (for graph target lines)
targetPressure: TimeSeries;
targetScrewRpm: TimeSeries;
targetNozzleTemperature: TimeSeries;
targetFrontTemperature: TimeSeries;
targetMiddleTemperature: TimeSeries;
targetBackTemperature: TimeSeries;
};
const { initialTimeSeries: pressure, insert: addPressure } = createTimeSeries();
@@ -253,6 +261,28 @@ const { initialTimeSeries: motorScrewRpm, insert: addMotorScrewRpm } =
const { initialTimeSeries: motorPower, insert: addMotorPower } =
createTimeSeries();
// Target value time series (for historical target lines on graphs)
const { initialTimeSeries: targetPressure, insert: addTargetPressure } =
createTimeSeries();
const { initialTimeSeries: targetScrewRpm, insert: addTargetScrewRpm } =
createTimeSeries();
const {
initialTimeSeries: targetNozzleTemperature,
insert: addTargetNozzleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetFrontTemperature,
insert: addTargetFrontTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetMiddleTemperature,
insert: addTargetMiddleTemperature,
} = createTimeSeries();
const {
initialTimeSeries: targetBackTemperature,
insert: addTargetBackTemperature,
} = createTimeSeries();
export function extruder3MessageHandler(
store: StoreApi<Extruder3NamespaceStore>,
throttledUpdater: ThrottledStoreUpdater<Extruder3NamespaceStore>,
@@ -271,6 +301,7 @@ export function extruder3MessageHandler(
if (eventName === "StateEvent") {
console.log(event);
const stateEvent = stateEventSchema.parse(event);
const timestamp = event.ts;
updateStore((state) => ({
...state,
state: stateEvent,
@@ -278,6 +309,43 @@ export function extruder3MessageHandler(
defaultState: stateEvent.data.is_default_state
? stateEvent
: state.defaultState,
// Update target value history
targetPressure: addTargetPressure(state.targetPressure, {
value: stateEvent.data.pressure_state.target_bar,
timestamp,
}),
targetScrewRpm: addTargetScrewRpm(state.targetScrewRpm, {
value: stateEvent.data.screw_state.target_rpm,
timestamp,
}),
targetNozzleTemperature: addTargetNozzleTemperature(
state.targetNozzleTemperature,
{
value: stateEvent.data.heating_states.nozzle.target_temperature,
timestamp,
},
),
targetFrontTemperature: addTargetFrontTemperature(
state.targetFrontTemperature,
{
value: stateEvent.data.heating_states.front.target_temperature,
timestamp,
},
),
targetMiddleTemperature: addTargetMiddleTemperature(
state.targetMiddleTemperature,
{
value: stateEvent.data.heating_states.middle.target_temperature,
timestamp,
},
),
targetBackTemperature: addTargetBackTemperature(
state.targetBackTemperature,
{
value: stateEvent.data.heating_states.back.target_temperature,
timestamp,
},
),
}));
} else if (eventName === "LiveValuesEvent") {
const liveValuesEvent = liveValuesEventSchema.parse(event);
@@ -378,6 +446,14 @@ export const createExtruder3NamespaceStore =
middlePower,
combinedPower,
totalEnergyKWh,
// Target value history
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
};
});
@@ -57,6 +57,14 @@ export function useExtruder3() {
backPower,
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
} = useExtruder3Namespace(machineIdentification);
// Single optimistic state for all state management
@@ -423,6 +431,14 @@ export function useExtruder3() {
combinedPower,
totalEnergyKWh,
// Target value history for graph target lines
targetPressure,
targetScrewRpm,
targetNozzleTemperature,
targetFrontTemperature,
targetMiddleTemperature,
targetBackTemperature,
// Loading states
isLoading: stateOptimistic.isOptimistic,
isDisabled: !stateOptimistic.isInitialized,
@@ -10,10 +10,17 @@ import React from "react";
import { useLaser1 } from "./useLaser1";
export function Laser1GraphsPage() {
const { diameter, x_diameter, y_diameter, roundness, state } = useLaser1();
const {
diameter,
x_diameter,
y_diameter,
roundness,
state,
targetDiameter: targetDiameterSeries,
} = useLaser1();
const syncHook = useGraphSync("diameter-roundness-group");
const targetDiameter = state?.laser_state?.target_diameter ?? 0;
const targetDiameterValue = state?.laser_state?.target_diameter ?? 0;
const lowerTolerance = state?.laser_state?.lower_tolerance ?? 0;
const higherTolerance = state?.laser_state?.higher_tolerance ?? 0;
@@ -71,21 +78,22 @@ export function Laser1GraphsPage() {
// Tolerance for diameter
{
type: "threshold" as const,
value: targetDiameter + higherTolerance,
value: targetDiameterValue + higherTolerance,
label: "Upper Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "threshold" as const,
value: targetDiameter - lowerTolerance,
value: targetDiameterValue - lowerTolerance,
label: "Lower Tolerance",
color: diameterColor,
dash: [5, 5],
},
{
type: "target" as const,
value: targetDiameter,
value: targetDiameterValue,
targetSeries: targetDiameterSeries,
label: "Target",
color: diameterColor,
},
@@ -64,6 +64,7 @@ export type Laser1NamespaceStore = {
x_diameter: TimeSeries;
y_diameter: TimeSeries;
roundness: TimeSeries;
targetDiameter: TimeSeries;
};
const { initialTimeSeries: diameter, insert: addDiameter } = createTimeSeries();
@@ -73,6 +74,8 @@ const { initialTimeSeries: y_diameter, insert: addYDiameter } =
createTimeSeries();
const { initialTimeSeries: roundness, insert: addRoundness } =
createTimeSeries();
const { initialTimeSeries: targetDiameter, insert: addTargetDiameter } =
createTimeSeries();
/**
* Factory function to create a new Laser1 namespace store
@@ -87,6 +90,7 @@ export const createLaser1NamespaceStore = (): StoreApi<Laser1NamespaceStore> =>
x_diameter,
y_diameter,
roundness,
targetDiameter,
};
});
@@ -117,6 +121,10 @@ export function laser1MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetDiameter: addTargetDiameter(state.targetDiameter, {
value: stateEvent.data.laser_state.target_diameter,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent
@@ -11,8 +11,15 @@ import { produce } from "immer";
function useLaser(machine_identification_unique: MachineIdentificationUnique) {
// Get consolidated state and live values from namespace
const { state, defaultState, diameter, x_diameter, y_diameter, roundness } =
useLaser1Namespace(machine_identification_unique);
const {
state,
defaultState,
diameter,
x_diameter,
y_diameter,
roundness,
targetDiameter,
} = useLaser1Namespace(machine_identification_unique);
// Single optimistic state for all state management
const stateOptimistic = useStateOptimistic<StateEvent>();
@@ -113,6 +120,7 @@ function useLaser(machine_identification_unique: MachineIdentificationUnique) {
x_diameter,
y_diameter,
roundness,
targetDiameter,
// Loading states
isLoading: stateOptimistic.isOptimistic,
@@ -19,6 +19,7 @@ export function Winder2GraphsPage() {
traversePosition,
tensionArmAngle,
pullerSpeed,
targetPullerSpeed,
spoolProgress,
} = useWinder2();
@@ -31,6 +32,7 @@ export function Winder2GraphsPage() {
<PullerSpeedGraph
syncHook={syncHook}
newData={pullerSpeed}
targetSeries={targetPullerSpeed}
targetSpeed={state?.puller_state?.target_speed}
unit="m/min"
renderValue={(value) => roundToDecimals(value, 0)}
@@ -243,12 +245,14 @@ export function SpoolProgressGraph({
export function PullerSpeedGraph({
syncHook,
newData,
targetSeries,
targetSpeed,
unit,
renderValue,
}: {
syncHook: ReturnType<typeof useGraphSync>;
newData: TimeSeries | null;
targetSeries?: TimeSeries;
targetSpeed?: number;
unit?: Unit;
renderValue?: (value: number) => string;
@@ -259,6 +263,7 @@ export function PullerSpeedGraph({
lines.push({
type: "target",
value: targetSpeed,
targetSeries,
label: "Target Speed",
color: "#06b6d4", // Match series color
});
@@ -62,6 +62,7 @@ export function useWinder2() {
defaultState,
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -638,6 +639,7 @@ export function useWinder2() {
// Individual live values (TimeSeries)
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -211,6 +211,7 @@ export type Winder2NamespaceStore = {
spoolRpm: TimeSeries;
tensionArmAngle: TimeSeries;
spoolProgress: TimeSeries;
targetPullerSpeed: TimeSeries;
};
//Store Factory and Message Handler -> no param, so default values
@@ -220,6 +221,8 @@ const { initialTimeSeries: traversePosition, insert: addTraversePosition } =
createTimeSeries();
const { initialTimeSeries: pullerSpeed, insert: addPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: targetPullerSpeed, insert: addTargetPullerSpeed } =
createTimeSeries();
const { initialTimeSeries: spoolRpm, insert: addSpoolRpm } = createTimeSeries();
const { initialTimeSeries: tensionArmAngle, insert: addTensionArmAngle } =
createTimeSeries();
@@ -239,6 +242,7 @@ export const createWinder2NamespaceStore =
// Time series data for live values
traversePosition,
pullerSpeed,
targetPullerSpeed,
spoolRpm,
tensionArmAngle,
spoolProgress,
@@ -277,6 +281,10 @@ export function winder2MessageHandler(
updateStore((state) => ({
...state,
state: stateEvent,
targetPullerSpeed: addTargetPullerSpeed(state.targetPullerSpeed, {
value: stateEvent.data.puller_state.target_speed,
timestamp: event.ts,
}),
// only set default state if is_default_state is true
defaultState: stateEvent.data.is_default_state
? stateEvent