diag delay so we can solve trendline slope problem

This commit is contained in:
Chris Amow 2026-08-24 01:43:35 -05:00
parent 01acdce045
commit 693d0873f8
5 changed files with 34 additions and 8 deletions

View file

@ -102,6 +102,10 @@ metadata remain authenticated. Inspect only a URL the user explicitly shares,
then immediately `DELETE /api/debug/captures/{id}`. The server also expires
captures after 24 hours and caps the directory at 50 files.
After the browser's required share picker closes, capture waits five seconds so
the user can restore a hover tooltip. `Alt+Shift+C` starts the same delayed flow
without clicking the status-bar button.
Diagnostic mode also shows a compact projection readout for visible manual
trendlines: historical/future canonical price changes, their screen slopes, and
whether the future canvas point exists. Include it in a capture when a line

View file

@ -697,6 +697,12 @@ inspect the pixels. `DELETE /api/debug/captures/{id}` is public too, so an agent
can remove an inspected screenshot immediately. The existing 24-hour expiry and
50-capture cap remain the backstop.
The screen-share picker steals focus, so an immediate frame cannot contain a
hover tooltip. Capture now waits five seconds after the picker closes and shows
the countdown in the status bar; the user can move back over the target before
zero. `Alt+Shift+C` starts the same flow from the keyboard, though the picker is
still mandatory browser security.
The public image and cleanup handlers live in `meta.py`, the deliberately
unauthenticated router. Capture creation and metadata remain in `routes.py`,
behind the normal chart authentication.

View file

@ -137,6 +137,7 @@ createApp({
const drawingTf = ref('all');
const diagnosticMode = ref(ConfluenceChart.diagnosticMode());
const captureBusy = ref(false);
const captureCountdown = ref(0);
const snap = ref(true);
const selectedDrawing = ref(null);
const selectedDrawings = ref([]);
@ -630,6 +631,12 @@ createApp({
if (video.requestVideoFrameCallback) video.requestVideoFrameCallback(() => resolve());
else requestAnimationFrame(() => resolve());
});
for (let seconds = 5; seconds > 0; seconds -= 1) {
captureCountdown.value = seconds;
await new Promise(resolve => setTimeout(resolve, 1000));
}
captureCountdown.value = 0;
await new Promise(resolve => requestAnimationFrame(resolve));
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
@ -671,6 +678,7 @@ createApp({
addEvent(cancelled ? 'CAPTURE CANCELLED' : `CAPTURE FAILED · ${error.message}`, 'capture-error');
} finally {
stream?.getTracks().forEach(track => track.stop());
captureCountdown.value = 0;
captureBusy.value = false;
}
}
@ -1306,6 +1314,12 @@ createApp({
}
function handleKeydown(event) {
if (diagnosticMode.value && event.altKey && event.shiftKey
&& event.code === 'KeyC' && !captureBusy.value) {
event.preventDefault();
captureDiagnostic();
return;
}
if (event.key === 'Escape') {
const palettes = [...document.querySelectorAll('.color-picker[open]')];
palettes.forEach(palette => { palette.open = false; });
@ -1542,6 +1556,6 @@ createApp({
window.removeEventListener('keydown', handleKeydown);
});
return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, activateDrawing, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo };
return { status, price, sessionOpen, quoteChange, animateCurrentPrice, autoScrollLivePrice, extraDetail, hideLowerTfDrawings, barAge, dataUpdatedAt, buildStamp, timeframe, timeframes, drawingColors, drawingColorRows, drawingColorName, colorRowLabels, symbolChoices, selectedSymbol, symbolColor, symbolScale, symbolScales, symbolPanelOpen, prefs, clusters, clustersByPrice, events, eventsMore, loadOlderEvents, diagnosticMode, captureBusy, captureCountdown, captureDiagnostic, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedDrawing, selectedDrawings, drawingList, startDrawingListResize, manualLines, hasDrawingSelection, allShownSelected, selectedAreHidden, alertPrice, alertNote, alertEarlyPoints, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, maValue, maAlertOn, toggleMaAlert, deleteSelected, deleteLine, toggleDrawingSelection, activateDrawing, toggleSelectAll, toggleSelectedVisibility, renameLine, updateLineStyle, updateLevelNumber, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, drawingTf, deleteDrawing, toggleComment, togglePinned, chooseSymbol, toggleSymbolPanel, startSymbolDrag, dropSymbol, optionExpirations, optionExpiryId, optionSide, optionMode, optionMin, optionMax, optionContracts, optionUnderlying, optionBusy, optionError, optionSearched, optionCopied, onOptionsToggle, searchOptions, copyOption, canUndo, undoTitle, undo };
},
}).mount('#app');

View file

@ -37,7 +37,7 @@
<span v-else-if="armedTool === 'fibonacci'" class="arm-hint">Drag from one swing to the other</span>
<span v-else-if="selectedDrawing" class="arm-hint">Drawing selected — Delete removes it</span>
<button v-if="diagnosticMode" class="diag-capture" :disabled="captureBusy" @click="captureDiagnostic">
{{ captureBusy ? 'CAPTURING…' : 'CAPTURE DIAGNOSTIC' }}
{{ captureCountdown ? `CAPTURE IN ${captureCountdown}…` : captureBusy ? 'CAPTURING…' : 'CAPTURE DIAGNOSTIC' }}
</button>
<span class="data-freshness"><span>BUILD <b>{{ buildStamp }}</b></span><span>UPDATED <b>{{ dataUpdatedAt }}</b></span></span>
</div>

View file

@ -72,10 +72,7 @@ test('an existing browser token is migrated once and removed from local storage'
});
test('diagnostic capture uploads a PNG and adds its capability ID to Events',
{
timeout: 180000,
skip: 'quarantined: display-media mocks and async upload completion are not yet reliable',
}, async () => {
{ timeout: 180000 }, async () => {
const { browser, page } = await launch();
let uploaded = null;
try {
@ -113,7 +110,10 @@ test('diagnostic capture uploads a PNG and adds its capability ID to Events',
});
HTMLMediaElement.prototype.play = async function play() {};
HTMLVideoElement.prototype.requestVideoFrameCallback = function callback(done) { done(); };
HTMLCanvasElement.prototype.getContext = () => ({ drawImage() {} });
const realGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function getContext(...args) {
return this.closest('#chart') ? realGetContext.apply(this, args) : { drawImage() {} };
};
HTMLCanvasElement.prototype.toBlob = function toBlob(done) {
done(new Blob([new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])], {
type: 'image/png',
@ -121,7 +121,9 @@ test('diagnostic capture uploads a PNG and adds its capability ID to Events',
};
});
await page.locator('.diag-capture').click();
await page.keyboard.press('Alt+Shift+C');
await page.waitForFunction(() =>
document.querySelector('.diag-capture')?.textContent.includes('CAPTURE IN'));
await page.waitForFunction(() =>
[...document.querySelectorAll('.alert-entry')]
.some(entry => entry.textContent.includes('c-TESTCAPTURE1')));