trendline wrinkle
This commit is contained in:
parent
333ca4c111
commit
edcfd3977e
9 changed files with 394 additions and 4 deletions
|
|
@ -10,7 +10,8 @@ debugging, because most entries describe something that looked like one bug and
|
||||||
turned out to be another. Then
|
turned out to be another. Then
|
||||||
[`docs/NEXT_STEPS.md`](docs/NEXT_STEPS.md) for current recommendations and known
|
[`docs/NEXT_STEPS.md`](docs/NEXT_STEPS.md) for current recommendations and known
|
||||||
deferred fixes. Mobile interaction work also has its own detailed plan in
|
deferred fixes. Mobile interaction work also has its own detailed plan in
|
||||||
[`docs/mobile_enhance.md`](docs/mobile_enhance.md).
|
[`docs/mobile_enhance.md`](docs/mobile_enhance.md). The CDN-to-Vite move is
|
||||||
|
[`docs/vite_build.md`](docs/vite_build.md).
|
||||||
|
|
||||||
## Tests earn their place by catching a real bug
|
## Tests earn their place by catching a real bug
|
||||||
|
|
||||||
|
|
@ -122,6 +123,9 @@ implemented in one go:
|
||||||
authenticated by OIDC. Read it before adding state to `Runtime`: new state is
|
authenticated by OIDC. Read it before adding state to `Runtime`: new state is
|
||||||
either genuinely shared (market data) or belongs to a user, and knowing which
|
either genuinely shared (market data) or belongs to a user, and knowing which
|
||||||
now is much cheaper than untangling it later.
|
now is much cheaper than untangling it later.
|
||||||
|
- `docs/vite_build.md` — pin and hash the frontend, stay on Coolify, do not
|
||||||
|
split components on the way. A production Dockerfile first, then Vite;
|
||||||
|
never a root `package.json` while nixpacks is still the builder.
|
||||||
|
|
||||||
Do not build local user accounts. The destination is OIDC, so password storage
|
Do not build local user accounts. The destination is OIDC, so password storage
|
||||||
would be written and then deleted.
|
would be written and then deleted.
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ and transport failure by carrying exact trade time, the last settled minute,
|
||||||
source receipt time and an application heartbeat over the existing WebSocket.
|
source receipt time and an application heartbeat over the existing WebSocket.
|
||||||
Any stale threshold must account for Yahoo's declared delay.
|
Any stale threshold must account for Yahoo's declared delay.
|
||||||
|
|
||||||
|
## Frontend build
|
||||||
|
|
||||||
|
The CDN-to-Vite plan is in `docs/vite_build.md`. First tranche is a production
|
||||||
|
`Dockerfile` that reproduces today's nixpacks image, so a later `package.json`
|
||||||
|
cannot make Coolify treat this as a Node app. Do not start the file move until
|
||||||
|
that deploy has been seen live.
|
||||||
|
|
||||||
## Mobile authoring
|
## Mobile authoring
|
||||||
|
|
||||||
The detailed plan is in `docs/mobile_enhance.md`. Recommended first tranche:
|
The detailed plan is in `docs/mobile_enhance.md`. Recommended first tranche:
|
||||||
|
|
|
||||||
|
|
@ -1042,3 +1042,18 @@ at `x=1082`, producing the long segment. The first fallback still failed because
|
||||||
interpolate between the surrounding displayed candles' own pixel coordinates,
|
interpolate between the surrounding displayed candles' own pixel coordinates,
|
||||||
which puts `:30` halfway between the hourly candles and remains correct even
|
which puts `:30` halfway between the hourly candles and remains correct even
|
||||||
when another series has inserted foreign timestamps into the shared scale.
|
when another series has inserted foreign timestamps into the shared scale.
|
||||||
|
|
||||||
|
### 2026-08-14 — a crease at the live edge was two renderers disagreeing
|
||||||
|
|
||||||
|
The canvas `LineSeries` holds the historical portion of a manual trendline; an
|
||||||
|
SVG segment projects it through future whitespace. `updateBar()` updated the
|
||||||
|
candle but did not extend the canvas line when a new displayed bar opened. The
|
||||||
|
SVG correctly started at that new bar, producing a visible crease at the live
|
||||||
|
edge even after its slope was corrected.
|
||||||
|
|
||||||
|
New displayed bars now append one source-space sample to each visible sloped
|
||||||
|
manual series. This is deliberately not a full `syncLevels()` call: the source
|
||||||
|
geometry and all previous line points are unchanged, and a per-line
|
||||||
|
`series.update()` keeps the canvas and SVG endpoints identical without adding
|
||||||
|
per-tick redraw work. Browser coverage opens a new 5m candle and asserts that
|
||||||
|
the canvas line reaches the same time and price as the projection.
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ See [§14](#14-why-execution-is-out-of-scope) for why, and for the seam left beh
|
||||||
> **For current work, start with `AGENTS.md`**, which every agent loads
|
> **For current work, start with `AGENTS.md`**, which every agent loads
|
||||||
> automatically. It points at the live planning documents:
|
> automatically. It points at the live planning documents:
|
||||||
> `docs/NEXT_STEPS.md` for the short list, `docs/async_refactor.md`,
|
> `docs/NEXT_STEPS.md` for the short list, `docs/async_refactor.md`,
|
||||||
> `docs/multi_user.md`, `docs/feature_undo.md` and `docs/mobile_enhance.md` for
|
> `docs/multi_user.md`, `docs/feature_undo.md`, `docs/mobile_enhance.md` and
|
||||||
> designs not yet built.
|
> `docs/vite_build.md` for designs not yet built.
|
||||||
>
|
>
|
||||||
> **`main` deploys to production.** A push triggers a Forgejo webhook and
|
> **`main` deploys to production.** A push triggers a Forgejo webhook and
|
||||||
> Coolify rebuild of <https://chart.amow.com>. That is the intended workflow now,
|
> Coolify rebuild of <https://chart.amow.com>. That is the intended workflow now,
|
||||||
|
|
@ -90,7 +90,7 @@ if your parser doesn't filter those, that fixture will catch it.
|
||||||
| Decision | Choice | Why |
|
| Decision | Choice | Why |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Backend | FastAPI (already scaffolded) | Repo already runs it; native WebSocket support |
|
| Backend | FastAPI (already scaffolded) | Repo already runs it; native WebSocket support |
|
||||||
| Frontend | Vue 3 from CDN, **no build step** | Matches existing `static/` setup; keeps deploy trivial |
|
| Frontend | Vue 3 from CDN, **no build step** | Matches existing `static/` setup; keeps deploy trivial. Destination is Vite — see `docs/vite_build.md`. Do not treat this row as a reason to reject that move. |
|
||||||
| Charting | TradingView Lightweight Charts **v5.2.0**, standalone build | Apache-2.0, canvas, built for incremental realtime updates |
|
| Charting | TradingView Lightweight Charts **v5.2.0**, standalone build | Apache-2.0, canvas, built for incremental realtime updates |
|
||||||
| Data source | **Pluggable `MarketDataSource`.** Yahoo first, Schwab later | Yahoo needs no API key *and* has the history Schwab lacks — see §2.1 |
|
| Data source | **Pluggable `MarketDataSource`.** Yahoo first, Schwab later | Yahoo needs no API key *and* has the history Schwab lacks — see §2.1 |
|
||||||
| Persistence | **In-memory first**, behind a `BarStore` interface | User confirmed deferring persistence is fine for v1 |
|
| Persistence | **In-memory first**, behind a `BarStore` interface | User confirmed deferring persistence is fine for v1 |
|
||||||
|
|
|
||||||
311
docs/vite_build.md
Normal file
311
docs/vite_build.md
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
# Vite build — from CDN script tags to a real frontend
|
||||||
|
|
||||||
|
**Status: tracked, not started.** A direction to refactor toward, not a project
|
||||||
|
with a date. Each phase below is worth shipping on its own; none of it is
|
||||||
|
speculative scaffolding for a component rewrite.
|
||||||
|
|
||||||
|
Today `static/index.html` loads Vue 3, Lightweight Charts 5.2.0 and Font Awesome
|
||||||
|
7.3.1 from unpkg, then two plain scripts. FastAPI serves those files and stamps
|
||||||
|
`?v=` onto every `/static/` URL. Production is Coolify + nixpacks + a Python
|
||||||
|
`Procfile`. That is the setup this document replaces.
|
||||||
|
|
||||||
|
## The goal is not "more Vue"
|
||||||
|
|
||||||
|
The target is **a pinned, hashed, same-origin frontend** that we can grow
|
||||||
|
without unpkg and without a Python hasher. It is not a component split, not
|
||||||
|
TypeScript, not a router, and not leaving Coolify.
|
||||||
|
|
||||||
|
- `chart.js` stays a plain class. Vue still must not wrap chart or series
|
||||||
|
objects in `ref()` / `reactive()`.
|
||||||
|
- `window.__chart` stays. E2E and diagnostic work depend on it.
|
||||||
|
- One `App.vue` holding today's template and `setup()`. Do not extract the
|
||||||
|
color picker or the tool panels in the same change.
|
||||||
|
- Stay on Coolify. The friction is nixpacks autodetection, not the platform.
|
||||||
|
|
||||||
|
## What is already right
|
||||||
|
|
||||||
|
- Vue 3 Composition API in `static/app.js` (`createApp`, `ref`, `computed`,
|
||||||
|
`watch`, `onMounted`). That maps 1:1 onto `vue` from npm.
|
||||||
|
- `ConfluenceChart` is already framework-free. It only needs `export` instead
|
||||||
|
of `window.ConfluenceChart`, and ESM named imports instead of the
|
||||||
|
`LightweightCharts` global.
|
||||||
|
- FastAPI already owns `/`, `/api`, `/ws`. The built SPA still comes from
|
||||||
|
that origin. Do not put a Vite server in production.
|
||||||
|
- Asset hashing exists because a tab left open kept running yesterday's JS
|
||||||
|
(`main.asset_version`, `tests/test_asset_versioning.py`). Vite's content
|
||||||
|
hashes replace that rewriter; the *reason* does not go away.
|
||||||
|
|
||||||
|
## Constraints this repo will punish you for forgetting
|
||||||
|
|
||||||
|
- **The agent is on a different machine from the user's browser.** Local Vite
|
||||||
|
on `localhost:5173` is invisible to them. Whatever serves the UI in dev must
|
||||||
|
still be reachable as `hera.local:8010` (or whatever host port compose
|
||||||
|
publishes). HMR has to work across that hop, or we do not use HMR.
|
||||||
|
- **`--reload` plus an 82-second seed.** Never put a scratch `.py` in the repo
|
||||||
|
root. Frontend files are safe; uvicorn watches Python. Do not "help" by
|
||||||
|
adding a Python build helper at the root.
|
||||||
|
- **Every push to `main` is a production deploy**, and a deploy restarts the
|
||||||
|
market stream. The Vite cutover is one of those deploys. Land the production
|
||||||
|
Dockerfile *before* a root `package.json` exists, or nixpacks may decide
|
||||||
|
this is a Node app and the site goes dark.
|
||||||
|
- **E2E hits `http://api:8000`**, waits on `window.__chart.bars`, and uses
|
||||||
|
`--lang=en-US`. None of that changes. A blank canvas after the move is
|
||||||
|
still the locale bug until proven otherwise.
|
||||||
|
- **Pin what unpkg currently pins.** Lightweight Charts **5.2.0** and Font
|
||||||
|
Awesome **7.3.1**. Vue's CDN tag is `vue@3` (floating). Pin a current Vue
|
||||||
|
3.x on the way in; do not upgrade LWC in this work. v5 series creation is
|
||||||
|
`chart.addSeries(CandlestickSeries, opts)` — the v4 helpers do not exist.
|
||||||
|
- **One uvicorn worker, forever**, until the streamer is a separate process.
|
||||||
|
The Dockerfile `CMD` is the `Procfile` line. Do not add `--workers`.
|
||||||
|
|
||||||
|
## Target layout
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
package.json
|
||||||
|
package-lock.json committed
|
||||||
|
vite.config.js
|
||||||
|
index.html Vite entry; empty #app
|
||||||
|
src/
|
||||||
|
main.js createApp(App).mount('#app')
|
||||||
|
App.vue today's markup + today's setup()
|
||||||
|
chart.js export class ConfluenceChart
|
||||||
|
style.css moved from static/
|
||||||
|
dist/ gitignored; Vite outDir, served by FastAPI
|
||||||
|
Dockerfile production; Coolify prefers this over nixpacks
|
||||||
|
```
|
||||||
|
|
||||||
|
`static/` goes away when FastAPI is serving `dist/` and the e2e suite is green.
|
||||||
|
Do not keep both as a fallback — a missed build would silently serve the CDN
|
||||||
|
app.
|
||||||
|
|
||||||
|
Suggested `frontend/src/main.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
|
import './style.css';
|
||||||
|
import App from './App.vue';
|
||||||
|
|
||||||
|
createApp(App).mount('#app');
|
||||||
|
```
|
||||||
|
|
||||||
|
Suggested chart import (names used today):
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
createChart,
|
||||||
|
CandlestickSeries,
|
||||||
|
HistogramSeries,
|
||||||
|
LineSeries,
|
||||||
|
LineStyle,
|
||||||
|
LineType,
|
||||||
|
CrosshairMode,
|
||||||
|
TickMarkType,
|
||||||
|
} from 'lightweight-charts';
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `export default { setup() { ... return { ... }; } }` in `App.vue`.
|
||||||
|
`<script setup>` is a rewrite of the return bag for no gain.
|
||||||
|
|
||||||
|
## Dev: same origin, same port
|
||||||
|
|
||||||
|
A Vite dev server on 5173 is the usual tutorial and the wrong default here.
|
||||||
|
The user's browser already has one URL. Adding a second public port, plus an
|
||||||
|
HMR websocket that has to reach a remote host, is how this loses a day.
|
||||||
|
|
||||||
|
**Default:** a Node sidecar runs `vite build --watch` into `dist/`. The
|
||||||
|
existing `api` service serves that directory at `/` exactly as production
|
||||||
|
will. Compose still publishes one port. Edits to `.vue` / `.js` / `.css`
|
||||||
|
rebuild hashed assets; the next refresh picks them up. No HMR, no second
|
||||||
|
origin, no proxy for `/ws`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
frontend:
|
||||||
|
image: node:22-alpine
|
||||||
|
working_dir: /app/frontend
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
command: sh -c "npm ci && npm run build -- --watch"
|
||||||
|
```
|
||||||
|
|
||||||
|
`Dockerfile.dev` stays Python-only. Do not install Node in the API image.
|
||||||
|
|
||||||
|
Optional later, not part of the move: `vite` with `server.host: true` and
|
||||||
|
`server.hmr` pointed at the machine the *browser* can see. Only worth it if
|
||||||
|
the watch-and-refresh loop is actually painful.
|
||||||
|
|
||||||
|
`vite.config.js` needs nothing clever for the default path — `base: '/'`,
|
||||||
|
`build.outDir` set so FastAPI and the watcher agree (repo-root `dist/` or
|
||||||
|
`frontend/dist/`, pick one and use it everywhere). No `/api` proxy until
|
||||||
|
someone runs the Vite dev server.
|
||||||
|
|
||||||
|
## Production: Dockerfile, not nixpacks
|
||||||
|
|
||||||
|
Coolify builds a Dockerfile if one exists, and ignores the `Procfile`. Land
|
||||||
|
that switch as its own deploy, *reproducing today's image*, before the
|
||||||
|
frontend exists:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, when `frontend/` exists, make it multi-stage:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:22-alpine AS frontend
|
||||||
|
WORKDIR /src
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
COPY --from=frontend /src/dist /app/dist
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a `.dockerignore` so `data/`, `.venv`, `node_modules`, `artifacts/` and
|
||||||
|
`.env` never enter the build context. A missing ignore is how the Schwab
|
||||||
|
token or the drawing store gets baked into an image.
|
||||||
|
|
||||||
|
Unchanged, and not Coolify's problem:
|
||||||
|
|
||||||
|
- env vars (`CHART_PASSWORD`, `LIVE_SOURCE`, Schwab keys, ntfy)
|
||||||
|
- the persistent volume at `/app/data`
|
||||||
|
- `SOURCE_COMMIT` → `/api/version` → `bin/wait-deploy`
|
||||||
|
- the domain registered as `chart.amow.com:8000` (Traefik target port)
|
||||||
|
|
||||||
|
The first multi-stage deploy will be a **cold** build (Node layer is new).
|
||||||
|
Expect the ~90s end of the current range, plus `npm ci`. The old container
|
||||||
|
keeps serving until the swap; ignore the usual one-minute 502.
|
||||||
|
|
||||||
|
## FastAPI after the cutover
|
||||||
|
|
||||||
|
`GET /` reads `dist/index.html` and still sends `Cache-Control: no-store`.
|
||||||
|
The document must never be cached, or the hashed filenames inside it are the
|
||||||
|
stale thing instead — same reason as today.
|
||||||
|
|
||||||
|
Mount Vite's hashed directory, not a rewrite pass:
|
||||||
|
|
||||||
|
```python
|
||||||
|
app.mount("/assets", StaticFiles(directory=DIST_DIR / "assets"), name="assets")
|
||||||
|
```
|
||||||
|
|
||||||
|
Those files can be cached for a long time (`immutable`, or a one-year
|
||||||
|
`max-age`). Vite changes the filename when the content changes.
|
||||||
|
|
||||||
|
Delete `asset_version()` and the `ASSET_REF` rewrite. They hash `static/*`
|
||||||
|
and would either no-op or stamp `?v=` onto URLs Vite already uniquely named.
|
||||||
|
|
||||||
|
`tests/test_asset_versioning.py` keeps its purpose, changes its evidence:
|
||||||
|
|
||||||
|
- `/` is `no-store` and references `/assets/…` with a content hash
|
||||||
|
- hashed asset URLs do not need `?v=`
|
||||||
|
- a rebuild after editing a frontend source file changes the hash in the
|
||||||
|
HTML (this one needs the built `dist/` in the test fixture, or a tiny
|
||||||
|
committed stub `dist/` used only by that test — do not hit `npm` from
|
||||||
|
pytest)
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
Each is independently deployable. Do not fold 2–4 into the Dockerfile PR.
|
||||||
|
|
||||||
|
### Phase 1 — Production Dockerfile, still CDN
|
||||||
|
|
||||||
|
Add `Dockerfile` + `.dockerignore`. Confirm `git push && bin/wait-deploy`
|
||||||
|
and `/api/version`. nixpacks is gone; the site is byte-identical.
|
||||||
|
|
||||||
|
This is the phase that makes a later `package.json` safe.
|
||||||
|
|
||||||
|
### Phase 2 — Scaffold `frontend/`, no cutover
|
||||||
|
|
||||||
|
`npm create vite@latest` (Vue, JS, no TS). Pin `vue`, `lightweight-charts@5.2.0`,
|
||||||
|
`@fortawesome/fontawesome-free@7.3.1`. Commit `package-lock.json`. Add
|
||||||
|
`node_modules/` and `dist/` to `.gitignore`.
|
||||||
|
|
||||||
|
Do not add `package.json` at the repo root. nixpacks is already gone after
|
||||||
|
phase 1; keep Node metadata under `frontend/` anyway so a future builder
|
||||||
|
cannot mis-detect the app.
|
||||||
|
|
||||||
|
### Phase 3 — Move the two files, same behaviour
|
||||||
|
|
||||||
|
- `static/chart.js` → `frontend/src/chart.js` with ESM imports and `export`.
|
||||||
|
Drop `window.ConfluenceChart`.
|
||||||
|
- `static/app.js` `setup()` + the `#app` inner HTML → `frontend/src/App.vue`.
|
||||||
|
`import { ConfluenceChart } from './chart.js'`. Keep assigning
|
||||||
|
`window.__chart = chartApi` in `onMounted`.
|
||||||
|
- `static/style.css` → `frontend/src/style.css`.
|
||||||
|
- Font Awesome via the npm CSS import, not the unpkg `<link>`.
|
||||||
|
|
||||||
|
The global Vue build includes the compiler. Vite's Vue plugin compiles SFCs
|
||||||
|
and ships the runtime-only build. That is why the markup has to live in
|
||||||
|
`App.vue` (or another compiled module), not as HTML children of `#app`.
|
||||||
|
|
||||||
|
`npm run build` locally. Open the `dist/` preview against a running API only
|
||||||
|
if you need a sanity check; the real proof is phase 4.
|
||||||
|
|
||||||
|
### Phase 4 — FastAPI serves `dist/`, delete `static/`
|
||||||
|
|
||||||
|
Point `index()` and the static mount at `dist/`. Add the compose `frontend`
|
||||||
|
watcher. Rewrite `test_asset_versioning.py`. Run pytest and `./bin/e2e`.
|
||||||
|
Delete `static/`. Update the Dockerfile to the multi-stage form. Update
|
||||||
|
README / `docs/plan.md` §1 and §9 so they no longer describe unpkg.
|
||||||
|
|
||||||
|
After this, a frontend change that is not rebuilt is not deployed. The
|
||||||
|
multi-stage `Dockerfile` is what builds it on Coolify. Locally the watcher
|
||||||
|
is what builds it. There is no third path.
|
||||||
|
|
||||||
|
## What not to do in this work
|
||||||
|
|
||||||
|
- Do not extract Vue components, add Pinia, Vue Router, or TypeScript.
|
||||||
|
- Do not upgrade Lightweight Charts.
|
||||||
|
- Do not put a Vite origin in production, or a second public port in compose.
|
||||||
|
- Do not leave Coolify, add workers, or move env/volume/TLS anywhere else.
|
||||||
|
- Do not keep `static/` as a fallback once `dist/` is the source of truth.
|
||||||
|
- Do not add a root `package.json` before phase 1 is live.
|
||||||
|
- Do not run `npm` from pytest or from the API container.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
Same commands as today, plus a frontend build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec chart-api-1 sh -c "cd /app && python -m pytest -q"
|
||||||
|
./bin/e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
E2E still waits on `window.__chart.bars`. If the canvas is blank, check
|
||||||
|
`--lang=en-US` before the bundler. If icons are missing, the FA CSS import
|
||||||
|
did not land. If drawings or the socket die, the page origin changed and
|
||||||
|
`/ws` is not on the same host.
|
||||||
|
|
||||||
|
After the first multi-stage deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push && bin/wait-deploy
|
||||||
|
curl -fsS https://chart.amow.com/api/health
|
||||||
|
curl -fsS https://chart.amow.com/api/version
|
||||||
|
```
|
||||||
|
|
||||||
|
View-source on `/` should show `/assets/…` with a hash and no unpkg script
|
||||||
|
tags. A hard refresh on a tab that was open across the deploy should pick
|
||||||
|
up the new JS without a `?v=` rewriter.
|
||||||
|
|
||||||
|
## When this is done
|
||||||
|
|
||||||
|
`docs/plan.md` §1 currently says "Vue 3 from CDN, **no build step**". That
|
||||||
|
row becomes the lie the day phase 4 ships — change it in the same commit,
|
||||||
|
along with §9's script-tag snippet and the README layout line for `static/`.
|
||||||
|
This file then becomes history, like M0–M10 in the plan.
|
||||||
|
|
@ -533,17 +533,30 @@ class ConfluenceChart {
|
||||||
// the source: a bar behind the last one has nothing to contribute.
|
// the source: a bar behind the last one has nothing to contribute.
|
||||||
const last = this.bars[this.bars.length - 1];
|
const last = this.bars[this.bars.length - 1];
|
||||||
if (last && bar.t < last.t) return;
|
if (last && bar.t < last.t) return;
|
||||||
|
const advanced = !last || bar.t > last.t;
|
||||||
this.keepLivePriceVisible(bar.c);
|
this.keepLivePriceVisible(bar.c);
|
||||||
this.candles.update(this.toCandle(bar));
|
this.candles.update(this.toCandle(bar));
|
||||||
this.updateCurrentPrice(bar.c);
|
this.updateCurrentPrice(bar.c);
|
||||||
this.volume.update(ConfluenceChart.toVolume(bar));
|
this.volume.update(ConfluenceChart.toVolume(bar));
|
||||||
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
|
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
|
||||||
else this.bars.push(bar);
|
else this.bars.push(bar);
|
||||||
|
if (advanced) this.appendManualLineSamples(bar);
|
||||||
this.renderAnchorHandles();
|
this.renderAnchorHandles();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
this.renderTrendlineProjections();
|
this.renderTrendlineProjections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendManualLineSamples(bar) {
|
||||||
|
const index = this.bars.length - 1;
|
||||||
|
for (const level of this.levels) {
|
||||||
|
if (level.kind !== 'manual' || level.hidden || ConfluenceChart.isFlat(level)
|
||||||
|
|| bar.t < level.anchor_t || bar.t > (level.cutoff_t ?? Infinity)) continue;
|
||||||
|
const entry = this.levelSeries.get(level.id);
|
||||||
|
const value = this.linePriceAtIndex(level, index);
|
||||||
|
if (entry && value != null) entry.series.update({ time: bar.t, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setComments(comments) {
|
setComments(comments) {
|
||||||
this.comments = comments;
|
this.comments = comments;
|
||||||
this.renderComments();
|
this.renderComments();
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,46 @@ test('future 5m projections keep their slope after the settlement break',
|
||||||
assert.ok(Math.abs(result.futureChange - result.historicalChange) < 1e-9,
|
assert.ok(Math.abs(result.futureChange - result.historicalChange) < 1e-9,
|
||||||
`future slope changed from ${result.historicalChange} to ${result.futureChange}`);
|
`future slope changed from ${result.historicalChange} to ${result.futureChange}`);
|
||||||
assertNoPageErrors(page, assert);
|
assertNoPageErrors(page, assert);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a new 5m candle extends the line before its future projection',
|
||||||
|
{ timeout: 180000 }, async () => {
|
||||||
|
await withChart(async page => {
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const c = window.__chart;
|
||||||
|
const step = 5 * 60;
|
||||||
|
const bars = Array.from({ length: 20 }, (_, index) => ({
|
||||||
|
tf: '5m', t: 100000 + index * step, o: 100, h: 101, l: 99, c: 100,
|
||||||
|
v: 1, closed: true,
|
||||||
|
}));
|
||||||
|
const line = {
|
||||||
|
id: 'handoff-probe', kind: 'manual', tf: '5m', side: 'support', label: 'probe',
|
||||||
|
anchor_t: bars[0].t, anchor_p: 100, last_t: bars[4].t,
|
||||||
|
slope: 1 / (4 * step), cutoff_t: null, hidden: false, color: '#65b7cf',
|
||||||
|
line_width: 2, provisional: false,
|
||||||
|
};
|
||||||
|
c.setBars(bars);
|
||||||
|
c.setTrendlineGeometry({
|
||||||
|
mode: 'source_tf', series: { '5m': { times: bars.map(bar => bar.t), duration: step } },
|
||||||
|
});
|
||||||
|
c.syncLevels([line]);
|
||||||
|
const next = { ...bars.at(-1), t: bars.at(-1).t + step };
|
||||||
|
c.updateBar(next);
|
||||||
|
const data = c.levelSeries.get(line.id).series.data();
|
||||||
|
return {
|
||||||
|
seriesLastTime: Number(data.at(-1).time),
|
||||||
|
seriesLastPrice: data.at(-1).value,
|
||||||
|
expectedTime: next.t,
|
||||||
|
expectedPrice: c.linePriceAtIndex(line, c.bars.length - 1),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.seriesLastTime, result.expectedTime,
|
||||||
|
'the canvas line stopped before the projection began');
|
||||||
|
assert.equal(result.seriesLastPrice, result.expectedPrice,
|
||||||
|
'the canvas line and projection disagreed at the live edge');
|
||||||
|
assertNoPageErrors(page, assert);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue