{
  "schema_version": 1,
  "channel": "homelab-runtime",
  "command_id": "GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z",
  "status": "OK",
  "rc": 0,
  "host": "pve01",
  "mode": "verify",
  "component": "gram1-site-pol-accordion-v22-read-only-exact-diag",
  "started_at_utc": "2026-08-01T09:08:23Z",
  "finished_at_utc": "2026-08-01T09:08:24Z",
  "reference_register_checked": true,
  "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e",
  "error_register_checked": true,
  "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83",
  "command_sha256": "e53767468737d29d53fc5cbdbbbd8fa6c8714ae560be2f80b167e03b95e9955a",
  "duplicate_failed_command_blocked": false,
  "block_reason": null,
  "execution_started": true,
  "change_declared": false,
  "result_contract_valid": true,
  "result_contract_status": null,
  "result_contract_error": null,
  "command_rc": 0,
  "changes_made": false,
  "rollback_started": false,
  "rollback_restored": null,
  "mutation_outcome": "NO_MUTATION",
  "sanitized": true,
  "secrets_included": false,
  "private_addresses_included": false,
  "raw_evidence_retained_locally": true,
  "raw_evidence_sha256": "d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16",
  "sanitized_output_sha256": "d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16",
  "output_truncated_in_json": false,
  "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt",
  "output": "HOMELAB_RESULT_CONTRACT={\"changes_made\":false,\"command_id\":\"GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z\",\"mutation_outcome\":\"NO_MUTATION\",\"rollback_restored\":null,\"rollback_started\":false,\"status\":\"OK\",\"version\":1}\n{\"changes_made\":false,\"command_id\":\"GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z\",\"control_plane_error\":null,\"gitea_changed\":false,\"mutation_outcome\":\"NO_MUTATION\",\"previous_capsule_sha256\":\"72e6f01ce9e44231fd3bb6be7db3d5883c605c255f1960e7ae16af928d3d89c6\",\"previous_command_id\":\"GRAM1-SITE-AVAX-LINK-V21-READ-ONLY-RUNTIME-VERIFY-20260731T222536Z\",\"previous_output_sha256\":\"f3d82cba2fff4b3a5d8a4d0875c4131c83f92f3fcc3f9f71b0fe15616fd6a7c5\",\"previous_runner_json_sha256\":\"49eb7f6c791af47776554b7a6709ef3c84dafb8c76283db387ade4c535952b30\",\"previous_runner_text_sha256\":\"ded9edcce6da02300a64c83d16f8f3c675b4bba3ab70b8e70adc4d2394309f00\",\"production_site_changed\":false,\"rc\":0,\"remote_result\":{\"changes_made\":false,\"checks\":{\"compose_sha256\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"data\":{\"files\":[{\"bytes\":20662,\"focus\":[{\"path\":\"entries.POL:1M:1000\",\"value\":\"dict\"},{\"path\":\"entries.POL:1M:1000.result.symbol\",\"value\":\"POL\"},{\"path\":\"entries.POL:1M:1000.result.pair\",\"value\":\"POLUSDT\"}],\"name\":\"klines-cache.json\",\"top_keys\":[\"entries\",\"saved_at\",\"version\"]},{\"bytes\":13860,\"focus\":[{\"path\":\"coins.chainlink.change_1h\",\"value\":\"-0.2\"},{\"path\":\"coins.chainlink.change_24h\",\"value\":\"-2.1\"},{\"path\":\"coins.chainlink.change_7d\",\"value\":\"-2.0\"},{\"path\":\"coins.hedera-hashgraph.change_1h\",\"value\":\"0.6\"},{\"path\":\"coins.hedera-hashgraph.change_24h\",\"value\":\"2.1\"},{\"path\":\"coins.hedera-hashgraph.change_7d\",\"value\":\"0.3\"},{\"path\":\"coins.avalanche-2.change_1h\",\"value\":\"-0.1\"},{\"path\":\"coins.avalanche-2.change_24h\",\"value\":\"-1.3\"},{\"path\":\"coins.avalanche-2.change_7d\",\"value\":\"2.2\"},{\"path\":\"coins.polygon-ecosystem-token\",\"value\":\"dict\"},{\"path\":\"coins.polygon-ecosystem-token.symbol\",\"value\":\"POL\"},{\"path\":\"coins.polygon-ecosystem-token.name\",\"value\":\"POL (ex-MATIC)\"},{\"path\":\"coins.polygon-ecosystem-token.change_1h\",\"value\":\"0.8\"},{\"path\":\"coins.polygon-ecosystem-token.change_24h\",\"value\":\"1.2\"},{\"path\":\"coins.polygon-ecosystem-token.change_7d\",\"value\":\"-5.9\"}],\"name\":\"market.json\",\"top_keys\":[\"coins\",\"currency\",\"updated_at\",\"usd_rub\"]},{\"bytes\":96,\"focus\":[{\"path\":\"alerts_triggered\",\"value\":\"0\"}],\"name\":\"worker-status.json\",\"top_keys\":[\"alerts_triggered\",\"coin_count\",\"history_rows\",\"status\",\"updated_at\"]}],\"now\":1785575304},\"exact_file_hashes\":{\"app.py\":\"ec226952cdb7fb58edbfc0173af47f4d901f3206e15169df12294983731fbc20\",\"templates/index.html\":\"4bff850e158898f09cbcaea38d39126c4de702f701a4f9c034d9e3a72002ff8b\",\"worker.py\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\"},\"facts\":{\"POL_in_index\":true,\"POL_in_market_data\":true,\"chart_layout_checked\":true,\"movement_section_checked\":true},\"next_decision\":\"BUILD_ONE_ATOMIC_POL_MOVEMENT_AND_ACCORDION_FIX_FROM_EXACT_CONTEXTS\",\"runtime\":{\"ports\":[8000,5000,8080,3000,80,39065],\"routes\":[{\"bytes\":1088,\"contains_POL\":false,\"content_type\":\"text/html; charset=utf-8\",\"focus\":[],\"path\":\"/login\",\"port\":8000,\"status\":200},{\"bytes\":31,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/logout\",\"port\":8000,\"status\":405},{\"bytes\":0,\"contains_POL\":false,\"content_type\":null,\"focus\":[],\"path\":\"/\",\"port\":8000,\"status\":303},{\"bytes\":66,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"market_age_seconds\",\"status\",\"type\"],\"path\":\"/health\",\"port\":8000,\"status\":200},{\"bytes\":22,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/coin-icon/{coin_id}\",\"port\":8000,\"status\":404},{\"bytes\":196203,\"contains_POL\":false,\"content_type\":\"application/javascript\",\"focus\":[\"/*! * @license * TradingView Lightweight Charts™ v5.2.0 * Copyright (c) 2026 TradingView, Inc. * Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0 */ !function(){\\\"use strict\\\";const t={title:\\\"\\\",visible:!0,hitTestTolerance:3,lastValueVisible:!0,priceLineVisible:!0,priceLineSource:0,priceLineWidth:1,priceLineColor:\\\"\\\",priceLineStyle:2,baseLineVisible:!0,baseLineWidth:1,baseLineColor:\\\"#B2B5BE\\\",baseLineStyle:0,priceFormat:{type:\\\"price\\\",precision:2,minMove:.01}};var i,n;function s(t,i){const n=function(t,i){switch(t){case 0:default:retu\",\"v-attr-logo{--fill:#131722;--stroke:#fff;position:absolute;left:10px;bottom:10px;height:19px;width:35px;margin:0;padding:0;border:0;z-index:3;}a#tv-attr-logo[data-dark]{--fill:#D1D4DC;--stroke:#131722;}\\\",this.Hv=document.createElement(\\\"a\\\"),this.Hv.href=`https://www.tradingview.com/?utm_medium=lwc-link&utm_campaign=lwc-chart${this.Jv()}`,this.Hv.title=\\\"Charting by TradingView\\\",this.Hv.id=\\\"tv-attr-logo\\\",this.Hv.target=\\\"_blank\\\",this.Hv.innerHTML='<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"35\\\" height=\\\"19\\\" fill=\\\"none\\\"><g fill-rule=\\\"evenodd\\\" clip-path=\\\"url(#a)\\\" clip-rule=\\\"evenodd\\\"><path fill=\\\"var(--stroke)\\\" d=\\\"M2 0H0v10h6v9h21.4l.5-1.3 6-15 1-2.7H23.7l-.5 1.3-.2.6a5 5 0 0 0-7-.9V0H2Zm20 17h4l5.2-13 .8-2h-7l-1 2.5-.2.5-1.5 3.8-.3.7V17Zm-.8-10a3 3 0 0 0 .7-2.7A3 3 0 1 0 16.8 7h4.4ZM14 7V2H2v6h6v9h4V7h2Z\\\"/><path fill=\\\"var(--fill)\\\" d=\\\"M14\",\"e:#fff;position:absolute;left:10px;bottom:10px;height:19px;width:35px;margin:0;padding:0;border:0;z-index:3;}a#tv-attr-logo[data-dark]{--fill:#D1D4DC;--stroke:#131722;}\\\",this.Hv=document.createElement(\\\"a\\\"),this.Hv.href=`https://www.tradingview.com/?utm_medium=lwc-link&utm_campaign=lwc-chart${this.Jv()}`,this.Hv.title=\\\"Charting by TradingView\\\",this.Hv.id=\\\"tv-attr-logo\\\",this.Hv.target=\\\"_blank\\\",this.Hv.innerHTML='<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"35\\\" height=\\\"19\\\" fill=\\\"none\\\"><g fill-rule=\\\"evenodd\\\" clip-path=\\\"url(#a)\\\" clip-rule=\\\"evenodd\\\"><path fill=\\\"var(--stroke)\\\" d=\\\"M2 0H0v10h6v9h21.4l.5-1.3 6-15 1-2.7H23.7l-.5 1.3-.2.6a5 5 0 0 0-7-.9V0H2Zm20 17h4l5.2-13 .8-2h-7l-1 2.5-.2.5-1.5 3.8-.3.7V17Zm-.8-10a3 3 0 0 0 .7-2.7A3 3 0 1 0 16.8 7h4.4ZM14 7V2H2v6h6v9h4V7h2Z\\\"/><path fill=\\\"var(--fill)\\\" d=\\\"M14 2H2v6h6v9h6V2Zm12 15h-7l6-15h7l-6\",\"exOf(\\\"win\\\")>=0);class Qn{constructor(t,i,n){var s;this.Gw=[],this.Xw=[],this.Jw=0,this.ho=0,this.C_=0,this.Qw=0,this.tg=0,this.ig=null,this.ng=!1,this.Wm=new o,this.Hm=new o,this.pd=new o,this.sg=null,this.eg=null,this.jv=t,this.yn=i,this.xu=n,this.Hv=document.createElement(\\\"div\\\"),this.Hv.classList.add(\\\"tv-lightweight-charts\\\"),this.Hv.style.overflow=\\\"hidden\\\",this.Hv.style.direction=\\\"ltr\\\",this.Hv.style.width=\\\"100%\\\",this.Hv.style.height=\\\"100%\\\",(s=this.Hv).style.userSelect=\\\"none\\\",s.style.webkitUserSelect=\\\"none\\\",s.style.msUserSelect=\\\"none\\\",s.style.MozUserSelect=\\\"none\\\",s.style.webkitTapHighlightColor=\\\"transparent\\\",this.rg=document.createElement(\\\"table\\\"),this.rg.setAttribute(\\\"cellspacing\\\",\\\"0\\\"),this.Hv.appendChild(this.rg),this.hg=this.ag.bind(this),ts(this.yn)&&this.lg(!0),this.sn=new Hi(this.md.bind(this),this.yn,n),this.Qt().Dd().i(this.\",\"paneIndex();i!==t&&(r(t>=0&&t<this.sv.rv().length,\\\"Invalid pane index\\\"),this.sv.Qt().Ad(i,t))}getSeries(){return this.yt.U_().map((t=>this.NM(t)))??[]}getHTMLElement(){const t=this.sv.rv();return t&&0!==t.length&&t[this.paneIndex()]?t[this.paneIndex()].uv():null}attachPrimitive(t){this.yt.hl(t),t.attached&&t.attached({chart:this.FM,requestUpdate:()=>this.yt.Qt().Pa()})}detachPrimitive(t){this.yt.al(t)}priceScale(t){if(null===this.yt.A_(t))throw new Error(`Cannot find price scale with id: ${t}`);return new te(this.sv,t,this.paneIndex())}setPreserveEmptyPane(t){this.yt.W_(t)}preserveEmptyPane(){return this.yt.H_()}addCustomSeries(t,i={},n=0){return this.FM.addCustomSeries(t,i,n)}addSeries(t,i={}){return this.FM.addSeries(t,i,this.paneIndex())}}const ne={color:\\\"#FF0000\\\",price:0,lineStyle:2,lineWidth:1,lineVisible:!0,axisLabelVisible:!0,\",\"(p(ne),t),n=this.ae.Va(i);return new se(n)}removePriceLine(t){this.ae.Ba(t.WM())}priceLines(){return this.ae.Aa().map((t=>new se(t)))}seriesType(){return this.ae.bh()}lastValueData(t){const i=this.ae.Ae(t);return i.ze?{noData:!0}:{noData:!1,price:i.gt,color:i.R}}attachPrimitive(t){this.ae.hl(t),t.attached&&t.attached({chart:this.FM,series:this,requestUpdate:()=>this.ae.Qt().Pa(),horzScaleBehavior:this.xu})}detachPrimitive(t){this.ae.al(t),t.detached&&t.detached(),this.ae.Qt().Pa()}getPane(){const t=this.ae,i=a(this.ae.Qt().Ks(t));return this.jM(i)}moveToPane(t){this.ae.Qt().nf(this.ae,t)}seriesOrder(){const t=this.ae.Qt().Ks(this.ae);return null===t?-1:t.U_().indexOf(this.ae)}setSeriesOrder(t){const i=this.ae.Qt().Ks(this.ae);null!==i&&i.du(this.ae,t)}YM(t){this.HM.v()&&this.HM.p(t)}}class re{constructor(t,i,n){this.GM=new o,this.Gu=\",\".sv.Qt().N().crosshair.mode,this.sv.vr({crosshair:{mode:2}})),s=this.sv.Sg(t)}finally{i||void 0===n||this.sv.Qt().vr({crosshair:{mode:n}})}return s}addPane(t=!1){const i=this.sv.Qt().af();return i.W_(t),this.fb(i)}removePane(t){this.sv.Qt().Ed(t)}swapPanes(t,i){this.sv.Qt().Bd(t,i)}autoSizeActive(){return this.sv.wg()}chartElement(){return this.sv.vv()}panes(){return this.sv.Qt().Zn().map((t=>this.fb(t)))}paneSize(t=0){const i=this.sv.Rg(t);return{height:i.height,width:i.width}}setCrosshairPosition(t,i,n){const s=this.tb.get(n);if(void 0===s)return;const e=this.sv.Qt().Ks(s);null!==e&&this.sv.Qt().qd(t,i,e)}clearCrosshairPosition(){this.sv.Qt().Yd(!0)}horzBehaviour(){return this.hb}ub(i,n={},s=0){r(void 0!==i.ob),function(t){if(void 0===t||\\\"custom\\\"===t.type)return;const i=t;void 0!==i.minMove&&void 0===i.precision&&(i.precision=Ui(i.\",\"ByIndex(e.time,0);null!==_&&Tr(o,e,_,l,n.fontSize,h,this.Te,this.qv)}this.xt=!1}}function Dr(t){return{...vr,...t}}class Ir{constructor(t){this.Yh=null,this.OS=[],this.US=[],this.$S=null,this.Te=null,this.qv=null,this.jS=!0,this.qS=null,this.YS=null,this.KS=null,this.ZS=!0,this.yn=Dr(t)}attached(t){this.GS(),this.qv=t.chart,this.Te=t.series,this.Yh=new Rr(this.Te,a(this.qv),this.yn),this.DS=t.requestUpdate,this.Te.subscribeDataChanged((t=>this.YM(t))),this.ZS=!0,this.pS()}pS(){this.DS&&this.DS()}detached(){this.Te&&this.$S&&this.Te.unsubscribeDataChanged(this.$S),this.qv=null,this.Te=null,this.Yh=null,this.$S=null}FS(t){this.ZS=!0,this.OS=t,this.GS(),this.jS=!0,this.YS=null,this.pS()}XS(){return this.OS}paneViews(){return this.Yh?[this.Yh]:[]}updateAllViews(){this.JS()}hitTest(t,i){return this.Yh?this.Yh.renderer()?.Qs(t,i)??null:nul\"],\"path\":\"/vendor/lightweight-charts.js\",\"port\":8000,\"status\":200},{\"bytes\":27,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/api/klines/{symbol}\",\"port\":8000,\"status\":401},{\"bytes\":454,\"contains_POL\":true,\"content_type\":\"application/json\",\"focus\":[\"{\\\"status\\\":\\\"OK\\\",\\\"library_ok\\\":true,\\\"disk_cache_present\\\":true,\\\"symbols\\\":{\\\"HBAR\\\":{\\\"status\\\":\\\"OK\\\",\\\"candles\\\":83,\\\"cache_age_seconds\\\":38927,\\\"stale\\\":true,\\\"source\\\":\\\"binance\\\"},\\\"POL\\\":{\\\"status\\\":\\\"OK\\\",\\\"candles\\\":23,\\\"cache_age_seconds\\\":38927,\\\"stale\\\":true,\\\"source\\\":\\\"binance\\\"},\\\"AVAX\\\":{\\\"status\\\":\\\"OK\\\",\\\"candles\\\":71,\\\"cache_age_seconds\\\":38927,\\\"stale\\\":true,\\\"source\\\":\\\"binance\\\"},\\\"LINK\\\":{\\\"status\\\":\\\"OK\\\",\\\"candles\\\":91,\\\"cache_age_seconds\\\":38927,\\\"stale\\\":true,\\\"source\\\":\\\"binance\\\"}},\\\"bad\\\":0}\"],\"json_top_keys\":[\"bad\",\"disk_cache_present\",\"library_ok\",\"status\",\"symbols\"],\"path\":\"/api/chart-health\",\"port\":8000,\"status\":200},{\"bytes\":27,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/api/state\",\"port\":8000,\"status\":401},{\"bytes\":31,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/api/holding\",\"port\":8000,\"status\":405},{\"bytes\":31,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/api/alert\",\"port\":8000,\"status\":405}]},\"source\":{\"app\":{\"assignments\":[{\"line\":13,\"names\":[\"COINS\"],\"value\":\"{\\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\", \\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\"}\"},{\"line\":139,\"names\":[\"KLINE_SYMBOLS\"],\"value\":\"{\\\"AVAX\\\": \\\"AVAXUSDT\\\", \\\"HBAR\\\": \\\"HBARUSDT\\\", \\\"LINK\\\": \\\"LINKUSDT\\\", \\\"POL\\\": \\\"POLUSDT\\\"}\"}],\"functions\":[{\"line\":16,\"name\":\"db\",\"source\":\"def db():\\n    c = sqlite3.connect(DB, timeout=10)\\n    c.row_factory = sqlite3.Row\\n    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n    c.commit()\\n    return c\"},{\"line\":53,\"name\":\"login_page\",\"source\":\"def login_page(request: Request):\\n    if request.session.get(\\\"auth\\\"):\\n        return RedirectResponse(\\\"/\\\", 303)\\n    return HTMLResponse(\\\"\\\"\\\"<!doctype html><html lang=\\\"ru\\\"><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title><style>body{margin:0;background:#071018;color:#f3f7fb;font:15px system-ui;min-height:100vh;display:grid;place-items:center}.c{width:min(390px,calc(100% - 40px));background:#101f2c;border:1px solid #203442;border-radius:20px;padding:28px;box-shadow:0 30px 90px #0008}h1{margin:0 0 6px}p{color:#91a7b8}.d{color:#63e6be}input,button{width:100%;box-sizing:border-box;border-radius:11px;padding:13px;margin-top:10px;font:inherit}input{background:#07131b;border:1px solid #294456;color:#fff}button{border:0;background:#63e6be;color:#071018;font-weight:800;cursor:pointer}.e{color:#ff6b81;min-height:20px}</style><div class=\\\"c\\\"><h1>Gram Crypto</h1><p class=\\\"d\\\">https://gram1.ru/</p><p>Введите пароль для доступа к криптопортфелю.</p><form method=\\\"post\\\"><input type=\\\"password\\\" name=\\\"password\\\" autocomplete=\\\"current-password\\\" autofocus required><div class=\\\"e\\\">\\\"\\\"\\\" + (\\\"Неверный пароль\\\" if request.query_params.get(\\\"bad\\\") else \\\"\\\") + \\\"\\\"\\\"</div><button>Войти</button></form></div></html>\\\"\\\"\\\")\"},{\"line\":93,\"name\":\"coin_icon\",\"source\":\"def coin_icon(coin_id: str):\\n    import html\\n    import os\\n    import re\\n    from pathlib import Path\\n    import httpx\\n    from fastapi import HTTPException\\n    from fastapi.responses import Response\\n    if not re.fullmatch(r\\\"[a-z0-9-]{1,64}\\\", coin_id):\\n        raise HTTPException(status_code=404)\\n    cache_dir = Path(\\\"/data/coin-icons\\\")\\n    cache_dir.mkdir(parents=True, exist_ok=True)\\n    body_path = cache_dir / (coin_id + \\\".img\\\")\\n    type_path = cache_dir / (coin_id + \\\".type\\\")\\n    if body_path.exists() and body_path.stat().st_size > 128:\\n        media_type = type_path.read_text().strip() if type_path.exists() else \\\"image/png\\\"\\n        return Response(body_path.read_bytes(), media_type=media_type, headers={\\\"Cache-Control\\\":\\\"public, max-age=86400\\\"})\\n    try:\\n        with httpx.Client(timeout=6, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n            response = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params={\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":coin_id,\\\"sparkline\\\":\\\"false\\\"})\\n            response.raise_for_status()\\n            rows = response.json()\\n            if not rows or not rows[0].get(\\\"image\\\"):\\n                raise ValueError(\\\"image URL missing\\\")\\n            image = client.get(rows[0][\\\"image\\\"])\\n            image.raise_for_status()\\n        media_type = (image.headers.get(\\\"content-type\\\") or \\\"image/png\\\").split(\\\";\\\", 1)[0]\\n        if not media_type.startswith(\\\"image/\\\") or len(image.content) <= 128:\\n            raise ValueError(\\\"invalid image\\\")\\n        tmp_path = body_path.with_suffix(\\\".tmp\\\")\\n        tmp_path.write_bytes(image.content)\\n        os.replace(tmp_path, body_path)\\n        type_path.write_text(media_type)\\n        return Response(image.content, media_type=media_type, headers={\\\"Cache-Control\\\":\\\"public, max-age=86400\\\"})\\n    except Exception:\\n        symbols = {\\\"bitcoin\\\":\\\"₿\\\",\\\"ethereum\\\":\\\"Ξ\\\",\\\"solana\\\":\\\"S\\\",\\\"binancecoin\\\":\\\"BNB\\\",\\\"ripple\\\":\\\"XRP\\\",\\\"cardano\\\":\\\"ADA\\\",\\\"hedera-hashgraph\\\":\\\"HBAR\\\",\\\"hedera\\\":\\\"HBAR\\\",\\\"avalanche-2\\\":\\\"AVAX\\\",\\\"chainlink\\\":\\\"LINK\\\",\\\"toncoin\\\":\\\"TON\\\",\\\"dogecoin\\\":\\\"Ð\\\"}\\n        label = symbols.get(coin_id, coin_id[:3].upper())\\n        palette = [\\\"#f7931a\\\",\\\"#627eea\\\",\\\"#14f195\\\",\\\"#f3ba2f\\\",\\\"#23292f\\\",\\\"#3468d4\\\",\\\"#6f5af5\\\"]\\n        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\\n        safe_label = html.escape(label)\\n        svg = f'<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"64\\\" height=\\\"64\\\" viewBox=\\\"0 0 64 64\\\"><circle cx=\\\"32\\\" cy=\\\"32\\\" r=\\\"31\\\" fill=\\\"{color}\\\"/><text x=\\\"32\\\" y=\\\"38\\\" text-anchor=\\\"middle\\\" font-family=\\\"Arial,sans-serif\\\" font-size=\\\"18\\\" font-weight=\\\"700\\\" fill=\\\"#fff\\\">{safe_label}</text></svg>'\\n        return Respon\"},{\"line\":142,\"name\":\"lightweight_charts_js\",\"source\":\"def lightweight_charts_js():\\n    if not LIGHTWEIGHT_JS.is_file():\\n        raise HTTPException(404, \\\"chart library missing\\\")\\n    return Response(LIGHTWEIGHT_JS.read_bytes(), media_type=\\\"application/javascript\\\", headers={\\\"Cache-Control\\\": \\\"public, max-age=31536000, immutable\\\"})\"},{\"line\":205,\"name\":\"fetch_klines\",\"source\":\"def fetch_klines(symbol, limit=365, interval=\\\"1d\\\"):\\n    key = str(symbol).upper()\\n    pair = KLINE_SYMBOLS.get(key)\\n    if not pair:\\n        raise HTTPException(404, \\\"unknown symbol\\\")\\n    interval = str(interval or \\\"1d\\\")\\n    if interval not in {\\\"1d\\\", \\\"1M\\\"}:\\n        raise HTTPException(400, \\\"unsupported interval\\\")\\n    limit = max(1, min(int(limit), 1000))\\n    if interval == \\\"1d\\\":\\n        limit = max(30, limit)\\n    cache_key = (key, interval, limit)\\n    now = time.time()\\n    cached = KLINE_CACHE.get(cache_key)\\n    if cached and now - cached[0] < KLINE_CACHE_TTL:\\n        result = dict(cached[1])\\n        result[\\\"source\\\"] = \\\"cache\\\"\\n        result[\\\"stale\\\"] = False\\n        result[\\\"cache_age_seconds\\\"] = int(now - cached[0])\\n        return result\\n    stale = cached if cached and now - cached[0] <= KLINE_CACHE_MAX_STALE else None\\n    remaining = limit\\n    end_time = None\\n    collected = []\\n    try:\\n        while remaining > 0:\\n            page_limit = min(1000 if interval == \\\"1M\\\" else 100, remaining)\\n            params = {\\\"symbol\\\": pair, \\\"interval\\\": interval, \\\"limit\\\": page_limit}\\n            if end_time is not None:\\n                params[\\\"endTime\\\"] = end_time\\n            query = urllib.parse.urlencode(params)\\n            request = urllib.request.Request(\\n                \\\"https://data-api.binance.vision/api/v3/klines?\\\" + query,\\n                headers={\\\"User-Agent\\\": \\\"crypto-portfolio/1.0\\\"},\\n            )\\n            rows = None\\n            last_error = None\\n            for attempt in range(3):\\n                try:\\n                    with urllib.request.urlopen(request, timeout=20) as response:\\n                        rows = json.load(response)\\n                    break\\n                except Exception as exc:\\n                    last_error = exc\\n                    if attempt < 2:\\n                        time.sleep(0.5 * (attempt + 1))\\n            if rows is None:\\n                raise last_error\\n            if not isinstance(rows, list) or not rows:\\n                break\\n            collected.extend(rows)\\n            remaining -= len(rows)\\n            if len(rows) < page_limit:\\n                break\\n            end_time = int(rows[0][0]) - 1\\n    except Exception as exc:\\n        if stale:\\n            result = dict(stale[1])\\n            result[\\\"source\\\"] = \\\"stale-cache\\\"\\n            result[\\\"stale\\\"] = True\\n            result[\\\"cache_age_seconds\\\"] = int(now - stale[0])\\n            return result\\n        raise HTTPException(502, \\\"market data unavailable: \\\" + type(exc).__name__)\\n    unique = {\\n        int(row[0]): row\\n        for row in collected\\n        if isinstance(row\"},{\"line\":313,\"name\":\"chart_health\",\"source\":\"def chart_health():\\n    now = time.time()\\n    details = {}\\n    bad = 0\\n    for symbol in (\\\"HBAR\\\", \\\"POL\\\", \\\"AVAX\\\", \\\"LINK\\\"):\\n        entry = KLINE_CACHE.get((symbol, \\\"1M\\\", 1000)) or KLINE_CACHE.get((symbol, \\\"1d\\\", 365))\\n        if not entry or not _valid_kline_result(entry[1]):\\n            details[symbol] = {\\\"status\\\": \\\"ERROR\\\", \\\"candles\\\": 0, \\\"cache_age_seconds\\\": None}\\n            bad += 1\\n            continue\\n        age = int(now - entry[0])\\n        expired = age > KLINE_CACHE_MAX_STALE\\n        details[symbol] = {\\n            \\\"status\\\": \\\"ERROR\\\" if expired else \\\"OK\\\",\\n            \\\"candles\\\": len(entry[1][\\\"candles\\\"]),\\n            \\\"cache_age_seconds\\\": age,\\n            \\\"stale\\\": age > KLINE_CACHE_TTL,\\n            \\\"source\\\": entry[1].get(\\\"source\\\", \\\"cache\\\"),\\n        }\\n        if expired:\\n            bad += 1\\n    library_ok = LIGHTWEIGHT_JS.is_file() and LIGHTWEIGHT_JS.stat().st_size > 100000\\n    if not library_ok:\\n        bad += 1\\n    payload = {\\n        \\\"status\\\": \\\"OK\\\" if bad == 0 else \\\"ERROR\\\",\\n        \\\"library_ok\\\": library_ok,\\n        \\\"disk_cache_present\\\": KLINE_CACHE_FILE.is_file(),\\n        \\\"symbols\\\": details,\\n        \\\"bad\\\": bad,\\n    }\\n    return JSONResponse(payload, status_code=200 if bad == 0 else 503)\"},{\"line\":347,\"name\":\"state\",\"source\":\"def state(request: Request):\\n    require_login(request)\\n    with db() as c:\\n        holdings = {r[\\\"coin_id\\\"]: {\\\"quantity\\\":r[\\\"quantity\\\"],\\\"avg_buy\\\":r[\\\"avg_buy\\\"]} for r in c.execute(\\\"SELECT * FROM holdings\\\")}\\n        alerts = [dict(r) for r in c.execute(\\\"SELECT * FROM alerts ORDER BY id DESC\\\")]\\n        history = {}\\n        z = {r[\\\"name\\\"] for r in c.execute(\\\"PRAGMA table_info(market_history)\\\")}\\n        cc = next((x for x in (\\\"coin_id\\\",\\\"coin\\\",\\\"asset_id\\\") if x in z),None); pc = next((x for x in (\\\"price\\\",\\\"current_price\\\",\\\"usd_price\\\",\\\"value\\\") if x in z),None); tc = next((x for x in (\\\"ts\\\",\\\"timestamp\\\",\\\"time\\\",\\\"created_at\\\",\\\"updated_at\\\") if x in z),None)\\n        if cc and pc and tc:\\n            q = f'SELECT \\\"{pc}\\\" p FROM market_history WHERE \\\"{cc}\\\"=? ORDER BY \\\"{tc}\\\"'\\n            for cid in COINS:\\n                v=[float(r[\\\"p\\\"]) for r in c.execute(q,(cid,)) if r[\\\"p\\\"] is not None]\\n                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\\n    # CRYPTO_HISTORY_STATE_V2\\n    return {\\\"coins\\\":COINS,\\\"market\\\":market_data(),\\\"holdings\\\":holdings,\\\"alerts\\\":alerts,\\\"history\\\":history}\"},{\"line\":377,\"name\":\"add_alert\",\"source\":\"async def add_alert(request: Request):\\n    require_login(request)\\n    x = await request.json()\\n    coin = str(x.get(\\\"coin_id\\\",\\\"\\\")); kind = str(x.get(\\\"kind\\\",\\\"\\\"))\\n    if coin not in COINS or kind not in {\\\"price_above\\\",\\\"price_below\\\",\\\"cap_above\\\",\\\"cap_below\\\",\\\"pump\\\",\\\"dump\\\"}:\\n        raise HTTPException(400,\\\"bad rule\\\")\\n    threshold = float(x.get(\\\"threshold\\\",0)); window = int(x.get(\\\"window_seconds\\\",0)); email = str(x.get(\\\"email\\\",\\\"\\\")).strip()\\n    if threshold <= 0 or \\\"@\\\" not in email or (kind in {\\\"pump\\\",\\\"dump\\\"} and window not in {300,900,1800,3600,14400,86400}):\\n        raise HTTPException(400,\\\"bad values\\\")\\n    with db() as c:\\n        cur = c.execute(\\\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\\\",(coin,kind,threshold,window,email)); c.commit()\\n    return {\\\"ok\\\":True,\\\"id\\\":cur.lastrowid}\"},{\"line\":391,\"name\":\"delete_alert\",\"source\":\"def delete_alert(alert_id: int, request: Request):\\n    require_login(request)\\n    with db() as c:\\n        c.execute(\\\"DELETE FROM alerts WHERE id=?\\\",(alert_id,)); c.commit()\\n    return {\\\"ok\\\":True}\"}],\"routes\":[{\"decorator\":\"app.get('/login', response_class=HTMLResponse)\",\"function\":\"login_page\",\"line\":53,\"path\":\"/login\"},{\"decorator\":\"app.post('/login')\",\"function\":\"login\",\"line\":59,\"path\":\"/login\"},{\"decorator\":\"app.post('/logout')\",\"function\":\"logout\",\"line\":74,\"path\":\"/logout\"},{\"decorator\":\"app.get('/', response_class=HTMLResponse)\",\"function\":\"home\",\"line\":79,\"path\":\"/\"},{\"decorator\":\"app.get('/health')\",\"function\":\"health\",\"line\":85,\"path\":\"/health\"},{\"decorator\":\"app.get('/coin-icon/{coin_id}')\",\"function\":\"coin_icon\",\"line\":93,\"path\":\"/coin-icon/{coin_id}\"},{\"decorator\":\"app.get('/vendor/lightweight-charts.js')\",\"function\":\"lightweight_charts_js\",\"line\":142,\"path\":\"/vendor/lightweight-charts.js\"},{\"decorator\":\"app.get('/api/klines/{symbol}')\",\"function\":\"klines\",\"line\":308,\"path\":\"/api/klines/{symbol}\"},{\"decorator\":\"app.get('/api/chart-health')\",\"function\":\"chart_health\",\"line\":313,\"path\":\"/api/chart-health\"},{\"decorator\":\"app.get('/api/state')\",\"function\":\"state\",\"line\":347,\"path\":\"/api/state\"},{\"decorator\":\"app.post('/api/holding')\",\"function\":\"save_holding\",\"line\":364,\"path\":\"/api/holding\"},{\"decorator\":\"app.post('/api/alert')\",\"function\":\"add_alert\",\"line\":377,\"path\":\"/api/alert\"}]},\"index\":{\"bytes\":29483,\"chart_contexts\":[{\"offset\":1179,\"pattern\":\"grid\",\"text\":\"ing:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{border:0;border-radius:10px;padding:10px 13px;background:var(--g);color:#071018;font-weight:800;cursor:pointer}.ghost{background:#122431;color:var(--t);border:1px solid var(--l)}.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}.card,.panel{border:1px solid var(--l);background:linear-gradient(155deg,var(--p),var(--p2));border-radius:16px;padding:16px}.card strong{display:block;font-size:23px;margin-top:7px}.coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}.coin{cursor:pointer}.coin.active{border-color:#4d829f}.coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-\"},{\"offset\":2384,\"pattern\":\"grid\",\"text\":\"oin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padding:12px;border:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--\"},{\"offset\":3480,\"pattern\":\"grid\",\"text\":\"rder:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-template-columns:1fr}.coins{grid-template-columns:repeat(2,1fr)}.moves{grid-template-columns:1fr}.grid4{grid-template-columns:1fr}.form{grid-template-columns:1fr}.full{grid-column:auto}}</style></head> <body><main class=\\\"app\\\"><div class=\\\"top\\\"><div><div class=\\\"muted\\\">https://gram1.ru/</div><\"},{\"offset\":4993,\"pattern\":\"chart\",\"text\":\"class=\\\"muted\\\">—</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Вложено</span><strong id=\\\"invested\\\">—</strong></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Прибыль / убыток</span><strong id=\\\"pnl\\\">—</strong><span id=\\\"pnlp\\\" class=\\\"muted\\\">0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section> <section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><div><h2 id=\\\"chartTitle\\\">График цены</h2><span class=\\\"muted\\\">7 дней</span></div><span id=\\\"marketStatus\\\" class=\\\"status\\\">Загрузка</span></div><svg id=\\\"chart\\\" class=\\\"chart\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\"></svg></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section> <section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div>\"},{\"offset\":11247,\"pattern\":\"chart\",\"text\":\"\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\")}function bindInputs(){document.querySelectorAll(\\\".qty,.avg\\\").forEach(x=>x.onchange=async()=>{let id=x.dataset.id;let q=document.querySelector(\\\".qty[data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"]\\\").value;let a=document.querySelector(\\\".avg[data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"]\\\").value;let avg=currency===\\\"RUB\\\"?Number(a)/fx():Number(a);await fetch(\\\"/api/holding\\\",{method:\\\"POST\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},body:JSON.stringify({coin_id:id,quantity:Number(q),avg_buy:avg})});load()})}function draw(){let c=coin();chartTitle.textContent=(c.name||selected)+\\\" · \\\"+state.coins[selected]+\\\"/\\\"+currency;let a=c.sparkline_7d||[];if(a.length<2){chart.innerHTML=\\\"<text x=\\\\\\\"20\\\\\\\" y=\\\\\\\"40\\\\\\\" fill=\\\\\\\"#8fa5b6\\\\\\\">История ещё загружается</text>\\\";return}let min=Math.min(...a),max=Math.max(...a),w=900,h=270,p=18;let pts=a.map((v,i)=>(p+i*(w-2*p)/(a.length-1)).toFixed(1)+\\\",\\\"+(p+(max-v)*(h-2*p)/(max-min||1)).toFixed(1)).join(\\\" \\\");chart.innerHTML=\\\"<defs><linearGradient id=\\\\\\\"g\\\\\\\"><stop stop-color=\\\\\\\"#63e6be\\\\\\\"/><stop offset=\\\\\\\"1\\\\\\\" stop-color=\\\\\\\"#69a7ff\\\\\\\"/></linea\"},{\"offset\":13767,\"pattern\":\"grid\",\"text\":\"method:\\\"POST\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},body:JSON.stringify({coin_id:aCoin.value,kind:aKind.value,threshold:Number(aThreshold.value),window_seconds:dynamic?Number(aWindow.value):0,email:aEmail.value})});if(!r.ok){alert(\\\"Проверьте параметры\\\")};aThreshold.value=\\\"\\\";load()};document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.onclick=()=>{currency=x.dataset.currency;localStorage.setItem(\\\"portfolio_currency\\\",currency);render()});load();setInterval(load,60000)</script><style>/* CRYPTO_UI_V2 */.coins{grid-template-columns:repeat(2,minmax(250px,1fr))!important;gap:14px!important}.coin{min-height:120px;display:flex!important;flex-direction:column;align-items:center;justify-content:center;text-align:center}.moves{grid-template-columns:1fr!important}.move{text-align:center;min-height:72px;display:flex;flex-direction:column;justify-content:center}.alert{display:grid!important;grid-template-columns:minmax(0,1fr) minmax(170px,auto) auto!important;align-items:center!important}.sale{padding:8px 12px;border:1px solid var(--l\"},{\"offset\":23769,\"pattern\":\"chart\",\"text\":\"entListener(\\\"click\\\",()=>{ setTimeout(refresh,400); setTimeout(refresh,1200); }); } refresh(); setInterval(refresh,15000); }; if(document.readyState===\\\"loading\\\")document.addEventListener(\\\"DOMContentLoaded\\\",install,{once:true});else install(); const redrawForCurrency=()=>{if(lastState)draw(lastState);}; document.querySelectorAll(\\\".currency-btn\\\").forEach(button=>button.addEventListener(\\\"click\\\",()=>setTimeout(redrawForCurrency,0))); })(); </script> <style id=\\\"TRADINGVIEW_THREE_CHARTS_STYLE_V1\\\">#chart{display:none!important}#tvCharts{display:grid;grid-template-columns:1fr;gap:18px;margin-top:12px}.tv-card{min-height:470px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}.tv-card-head{padding:12px 14px 0;font-weight:800}.tv-widget-slot{height:430px;width:100%}@media(max-width:750px){.tv-widget-slot{height:370px}}</style> <script src=\\\"/vendor/lightweight-charts.js\\\"></script> <script id=\\\"LOCAL_TRADINGVIEW_CHARTS_V1\\\"> (()=>{ const symbols=[\\\"HBAR\\\",\\\"POL\\\",\\\"AVAX\\\",\\\"LINK\"},{\"offset\":24848,\"pattern\":\"lightweight\",\"text\":\"loadCandles=async symbol=>{ const response=await fetch(\\\"/api/klines/\\\"+encodeURIComponent(symbol)+\\\"?limit=1000&interval=1M\\\",{cache:\\\"no-store\\\"}); if(response.status===401){location=\\\"/login\\\";throw new Error(\\\"Требуется повторный вход\\\")} if(!response.ok)throw new Error(\\\"HTTP \\\"+response.status); const data=await response.json(); if(data.status!==\\\"OK\\\"||!Array.isArray(data.candles)||!data.candles.length)throw new Error(\\\"Нет данных\\\"); return data.candles; }; const mount=async()=>{ if(!window.LightweightCharts)throw new Error(\\\"Локальная библиотека не загрузилась\\\"); chart.style.display=\\\"none\\\"; const panel=chart.closest(\\\".panel\\\"); if(panel)panel.style.gridColumn=\\\"1 / -1\\\"; chartTitle.textContent=\\\"Графики монет\\\"; const note=chartTitle.parentElement.querySelector(\\\".muted\\\"); if(note)note.textContent=\\\"Дневные свечи Binance · 365 дней\\\"; let root=document.getElementById(\\\"tvCharts\\\"); if(!root){root=document.createElement(\\\"div\\\");root.id=\\\"tvCharts\\\";chart.insertAdjacentElement(\\\"afterend\\\",root)}\"},{\"offset\":25964,\"pattern\":\"lightweight\",\"text\":\"const card=document.createElement(\\\"div\\\"); card.className=\\\"tv-card\\\"; card.innerHTML=`<div class=\\\"tv-card-head\\\">${symbol} / USDT</div><div class=\\\"tv-widget-slot\\\"></div><div class=\\\"tv-chart-error muted\\\" style=\\\"padding:0 14px 12px\\\">Загрузка...</div>`; root.appendChild(card); const box=card.querySelector(\\\".tv-widget-slot\\\"); const error=card.querySelector(\\\".tv-chart-error\\\"); const api=LightweightCharts.createChart(box,{width:box.clientWidth||900,height:430,layout:{background:{type:LightweightCharts.ColorType.Solid,color:\\\"#081823\\\"},textColor:\\\"#8fa5b6\\\"},grid:{vertLines:{color:\\\"rgba(143,165,182,.08)\\\"},horzLines:{color:\\\"rgba(143,165,182,.08)\\\"}},rightPriceScale:{borderColor:\\\"rgba(143,165,182,.2)\\\"},timeScale:{borderColor:\\\"rgba(143,165,182,.2)\\\",timeVisible:false,secondsVisible:false}}); const series=api.addSeries(LightweightCharts.CandlestickSeries,{upColor:\\\"#63e6be\\\",downColor:\\\"#ff6b81\\\",wickUpColor:\\\"#63e6be\\\",wickDownColor:\\\"#ff6b81\\\",borderVisible:false,priceFormat:{type:\\\"price\\\",precision:8,minMove:.00000\"},{\"offset\":27037,\"pattern\":\"chart\",\"text\":\"server=new ResizeObserver(entries=>{const width=Math.floor(entries[0].contentRect.width);if(width>0)api.applyOptions({width})}); observer.observe(box); return {symbol,api,series,error,observer}; }); instances.push(...pending); await Promise.all(pending.map(async item=>{ try{ item.series.setData(await loadCandles(item.symbol)); item.api.timeScale().fitContent(); item.error.textContent=\\\"\\\"; }catch(chartError){ item.error.textContent=\\\"Ошибка: \\\"+String(chartError.message||chartError); } })); }; draw=()=>{}; mount().catch(error=>{ console.error(\\\"Local TradingView chart error\\\",error); const note=chartTitle.parentElement.querySelector(\\\".muted\\\"); if(note)note.textContent=\\\"Ошибка графиков: \\\"+String(error.message||error); }); })(); </script> <style id=\\\"SHARP_MOVES_LAYOUT_V2\\\">.sharp-moves-panel{grid-column:1 / -1;justify-self:center;width:min(100%,860px);margin-inline:auto}.sharp-moves-panel .title{justify-content:center}.sharp-moves-panel .tit\"},{\"offset\":28101,\"pattern\":\"chart\",\"text\":\"xt-align:center}#moves{grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:12px!important}@media(max-width:750px){.sharp-moves-panel{width:100%}#moves{grid-template-columns:1fr!important}}</style> <script id=\\\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\\\">(()=>{const move=()=>{const chart=document.getElementById(\\\"chart\\\");const panel=chart&&chart.closest(\\\".panel\\\");const main=document.querySelector(\\\"main\\\");if(!panel||!main)return;main.appendChild(panel);panel.style.gridColumn=\\\"1 / -1\\\";const title=document.getElementById(\\\"chartTitle\\\");if(title)title.textContent=\\\"Графики монет\\\";const note=panel.querySelector(\\\".muted\\\");if(note)note.textContent=\\\"Свечи Binance · 1 месяц · весь доступный период\\\"};document.addEventListener(\\\"DOMContentLoaded\\\",move);window.addEventListener(\\\"load\\\",move);setTimeout(move,300);setTimeout(move,1500);setTimeout(move,4000)})();</script></body></html>\"}],\"counts\":{\"POL\":1,\"chart\":37,\"movement_terms\":144},\"css_rules\":[\".grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}\",\".coins{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;margin:14px 0}\",\".coin-head{display:grid;grid-template-columns:26px minmax(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}\",\".coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}\",\".coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}\",\".coin-price{grid-area:price;font-size:17px}\"],\"declarations\":[\"const coin=()=>state.market.coins[selected];\",\"let m=state.market.coins||{};\",\"let ids=Object.keys(state.coins);\",\"let head=x.querySelector(\\\".coin-head\\\");\",\"let symbol=td.querySelector(\\\"b\\\");\",\"let c=coin();\",\"let a=Object.values(state.market.coins||{}).map(c=>({s:c.symbol,p:Number(c.change_1h||0),price:c.price})).sort((x,y)=>Math.abs(y.p)-Math.abs(x.p)).slice(0,3);\",\"let c=state.coins[a.coin_id];\",\"let r=await fetch(\\\"/api/alert\\\",{method:\\\"POST\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},body:JSON.stringify({coin_id:aCoin.value,kind:aKind.value,threshold:Number(aThreshold.value),window_seconds:dynamic?Number(aWindow.value):0,email:aEmail.value})});\",\"const install=()=>{ const button=document.getElementById(\\\"addAlert\\\");\",\"const coinEl=document.getElementById(\\\"aCoin\\\");\",\"let statusEl=document.getElementById(\\\"alertCreateStatusV3\\\");\",\"const ensureOptions=()=>{ try{ if(typeof state!==\\\"undefined\\\"&&state&&state.coins){ const ids=Object.keys(state.coins);\",\"const payload={ coin_id:String(coinEl.value||\\\"\\\"), kind:String(kindEl.value||\\\"\\\"), threshold:threshold, window_seconds:dynamic?Number(windowEl.value||0):0, email:String(emailEl.value||\\\"\\\").trim() };\",\"const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\",\"const response=await fetch(\\\"/api/alert\\\",{ method:\\\"POST\\\", credentials:\\\"same-origin\\\", headers:{\\\"Content-Type\\\":\\\"application/json\\\",\\\"Accept\\\":\\\"application/json\\\"}, body:JSON.stringify(payload) });\",\"const created=(fresh.alerts||[]).find(a=>{ const id=Number(a.id);\"],\"functions\":[\"function render(){let m=state.market.coins||{}\",\"function draw(){let c=coin();chartTitle.textContent=(c.name||selected)+\\\" · \\\"+state.coins[selected]+\\\"/\\\"+currency;let a=c.sparkline_7d||[];if(a.length<2){chart.innerHTML=\\\"<text x=\\\\\\\"20\\\\\\\" y=\\\\\\\"40\\\\\\\" fill=\\\\\\\"#8fa5b6\\\\\\\">История ещё загружается</text>\\\";return}\",\"function renderMoves(){let a=Object.values(state.market.coins||{}\",\"function label(a){let c=state.coins[a.coin_id];let k={price_above:\\\"цена ≥\\\",price_below:\\\"цена ≤\\\",cap_above:\\\"капитализация ≥\\\",cap_below:\\\"капитализация ≤\\\",pump:\\\"памп ≥ +\\\",dump:\\\"падение ≥ −\\\"}\",\"function renderAlerts(){alertList.innerHTML=state.alerts.length?state.alerts.map(a=>\\\"<div class=\\\\\\\"alert\\\\\\\"><div><b>\\\"+label(a)+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+a.email+\\\"</div></div><button class=\\\\\\\"btn ghost del\\\\\\\" data-id=\\\\\\\"\\\"+a.id+\\\"\\\\\\\">Удалить</button></div>\\\").join(\\\"\\\"):\\\"<span class=\\\\\\\"muted\\\\\\\">Оповещений пока нет</span>\\\";document.querySelectorAll(\\\".del\\\").forEach(x=>x.onclick=async()=>{await fetch(\\\"/api/alert/\\\"+x.dataset.id,{method:\\\"DELETE\\\"}\"],\"lines\":276,\"movement_contexts\":[{\"offset\":478,\"pattern\":\"top\",\"text\":\"<!doctype html> <html lang=\\\"ru\\\"><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>Gram Crypto</title> <style>:root{--bg:#071018;--p:#0f1d29;--p2:#0a1620;--l:#213644;--t:#f3f7fb;--m:#8fa5b6;--g:#63e6be;--r:#ff6b81;--b:#69a7ff}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 85% 0,#17395a55,transparent 32%),var(--bg);color:var(--t);font:14px system-ui}.app{max-width:1500px;margin:auto;padding:24px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.top-actions{display:flex;align-items:center;gap:10px}.currency-switch{display:flex;gap:4px;padding:4px;border:1px solid var(--l);border-radius:12px;background:#091722}.currency-btn{border:0;border-radius:8px;padding:7px 10px;background:transparent;color:var(--m);font-weight:800;cursor:pointer}.currency-btn.active{background:var(--b);color:#071018}h1{margin:0;font-size:28px}.muted{color:var(--m)}button,input,select{font:inherit}.btn{b\"},{\"offset\":2135,\"pattern\":\"top\",\"text\":\"(0,1fr);grid-template-areas:\\\"icon name\\\" \\\". price\\\";align-items:center;column-gap:9px;row-gap:7px;margin-bottom:6px}.coin-icon{grid-area:icon;width:26px;height:26px;border-radius:50%;object-fit:cover;background:#132635;box-shadow:0 0 12px #69a7ff66}.coin-symbol{grid-area:name;font-size:22px;font-weight:900;letter-spacing:.7px;color:var(--g);text-shadow:0 0 12px #63e6be66}.coin-price{grid-area:price;font-size:17px}.asset-cell{position:relative;padding-left:43px}.asset-cell>.table-coin-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:23px;height:23px}.asset-symbol{font-size:17px;font-weight:900;letter-spacing:.3px;color:var(--g);text-shadow:0 0 10px #63e6be55}.coin b{font-size:17px}.good{color:var(--g)}.bad{color:var(--r)}.layout{display:grid;grid-template-columns:1.6fr .8fr;gap:14px}.title{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.title h2{font-size:16px;margin:0}.chart{width:100%;height:270px}.moves{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.move{padd\"},{\"offset\":3188,\"pattern\":\"alert\",\"text\":\"order:1px solid var(--l);border-radius:12px;background:#091722}.table{overflow:auto}table{width:100%;border-collapse:collapse;min-width:900px}th,td{text-align:left;padding:10px;border-bottom:1px solid var(--l)}th{font-size:11px;color:var(--m)}input,select{background:#08141d;color:var(--t);border:1px solid var(--l);border-radius:9px;padding:9px}.num{width:115px}.alerts{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:14px}.form{display:grid;grid-template-columns:1fr 1fr;gap:9px}.full{grid-column:1/-1}.alert{display:flex;justify-content:space-between;gap:10px;border:1px solid var(--l);border-radius:11px;padding:11px;margin-bottom:8px;background:#091722}.hidden{display:none}.status{font-size:12px;padding:7px 10px;border:1px solid var(--l);border-radius:999px}@media(max-width:1050px){.coins{grid-template-columns:repeat(4,1fr)}.grid4{grid-template-columns:repeat(2,1fr)}}@media(max-width:750px){.app{padding:14px}.top{align-items:flex-start;gap:12px}.top-actions{flex-wrap:wrap;justify-content:flex-end}.layout,.alerts{grid-\"},{\"offset\":5229,\"pattern\":\"sharp\",\"text\":\"0%</span></div><div class=\\\"card\\\"><span class=\\\"muted\\\">Обновление рынка</span><strong id=\\\"updated\\\">—</strong><span class=\\\"muted\\\">каждые 5 минут</span></div></section> <section id=\\\"coins\\\" class=\\\"coins\\\"></section><section class=\\\"layout\\\"><div class=\\\"panel\\\"><div class=\\\"title\\\"><div><h2 id=\\\"chartTitle\\\">График цены</h2><span class=\\\"muted\\\">7 дней</span></div><span id=\\\"marketStatus\\\" class=\\\"status\\\">Загрузка</span></div><svg id=\\\"chart\\\" class=\\\"chart\\\" viewBox=\\\"0 0 900 270\\\" preserveAspectRatio=\\\"none\\\"></svg></div><div class=\\\"panel sharp-moves-panel\\\"><div class=\\\"title\\\"><h2>Резкие движения</h2></div><div id=\\\"moves\\\" class=\\\"moves\\\"></div></div></section> <section class=\\\"panel\\\" style=\\\"margin-top:14px\\\"><div class=\\\"title\\\"><div><h2>Позиции портфеля</h2><span class=\\\"muted\\\">Количество и средняя цена покупки</span></div></div><div class=\\\"table\\\"><table><thead><tr><th>Актив</th><th>Цена</th><th>Капитализация</th><th>Количество</th><th id=\\\"avgHeader\\\">Средняя покупка (USD)</th><th>Вложено</th><th>Сейчас</th><th>P/L</th></tr></thead><tbody id=\\\"rows\\\"></tbody></tab\"},{\"offset\":6674,\"pattern\":\"alert\",\"text\":\"кий памп</option><option value=\\\"dump\\\">Резкое падение</option></select><input id=\\\"aThreshold\\\" type=\\\"number\\\" step=\\\"any\\\" placeholder=\\\"Порог\\\"><select id=\\\"aWindow\\\" class=\\\"hidden\\\"><option value=\\\"300\\\">5 минут</option><option value=\\\"900\\\" selected>15 минут</option><option value=\\\"1800\\\">30 минут</option><option value=\\\"3600\\\">1 час</option><option value=\\\"14400\\\">4 часа</option><option value=\\\"86400\\\">24 часа</option></select><input id=\\\"aEmail\\\" class=\\\"full\\\" type=\\\"email\\\" value=\\\"aleisaev@yandex.ru\\\" placeholder=\\\"Email\\\"><button id=\\\"addAlert\\\" class=\\\"btn full\\\">Создать оповещение</button></div></div><div class=\\\"panel\\\"><div class=\\\"title\\\"><h2>Мои оповещения</h2></div><div id=\\\"alertList\\\"></div></div></section></main> <script>let state=null;let selected=\\\"hedera-hashgraph\\\";let currency=localStorage.getItem(\\\"portfolio_currency\\\")===\\\"RUB\\\"?\\\"RUB\\\":\\\"USD\\\";const fx=()=>Number(state&&state.market&&state.market.usd_rub||0);const cv=n=>Number(n||0)*(currency===\\\"RUB\\\"?fx():1);const money=(n,d=2)=>new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{style:\\\"currency\\\",cur\"},{\"offset\":7796,\"pattern\":\"alert\",\"text\":\");const compactMoney=n=>(currency===\\\"RUB\\\"?\\\"₽\\\":\\\"$\\\")+new Intl.NumberFormat(currency===\\\"RUB\\\"?\\\"ru-RU\\\":\\\"en-US\\\",{notation:\\\"compact\\\",maximumFractionDigits:2}).format(cv(n));const inputPrice=n=>Number((Number(n||0)*(currency===\\\"RUB\\\"?fx():1)).toFixed(currency===\\\"RUB\\\"?2:8));const coin=()=>state.market.coins[selected];async function load(){let r=await fetch(\\\"/api/state\\\");if(r.status===401){location=\\\"/login\\\";return}state=await r.json();render()}function render(){let m=state.market.coins||{};let ids=Object.keys(state.coins);/* ALERT_COIN_SELECTOR_EARLY_V1 */aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\");if(currency===\\\"RUB\\\"&&!(fx()>0)){currency=\\\"USD\\\"}document.querySelectorAll(\\\".currency-btn\\\").forEach(x=>x.classList.toggle(\\\"active\\\",x.dataset.currency===currency));avgHeader.textContent=\\\"Средняя покупка (\\\"+currency+\\\")\\\";coins.innerHTML=ids.map(id=>{let c=m[id]||{};let ch=Number(c.change_24h||0);return \\\"<div class=\\\\\\\"card coin \\\"+(id===selected?\\\"active\\\":\\\"\\\")+\\\"\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"><div class=\\\\\\\"coin-head\\\\\\\"\"},{\"offset\":9163,\"pattern\":\"change\",\"text\":\"id=x.dataset.id;let head=x.querySelector(\\\".coin-head\\\");let img=document.createElement(\\\"img\\\");img.className=\\\"coin-icon\\\";img.src=\\\"/coin-icon/\\\"+id;img.alt=state.coins[id];img.loading=\\\"lazy\\\";head.prepend(img);x.onclick=()=>{selected=id;render()}});let total=0,inv=0,daily=0;rows.innerHTML=ids.map(id=>{let c=m[id]||{};let h=state.holdings[id]||{quantity:0,avg_buy:0};let value=Number(h.quantity)*Number(c.price||0);let cost=Number(h.quantity)*Number(h.avg_buy);let p=value-cost;total+=value;inv+=cost;daily+=value*Number(c.change_24h||0)/100;return \\\"<tr><td><b>\\\"+state.coins[id]+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+(c.name||id)+\\\"</div></td><td>\\\"+money(c.price,2)+\\\"</td><td>\\\"+money(c.market_cap,0)+\\\"</td><td><input class=\\\\\\\"num qty\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+h.quantity+\\\"\\\\\\\"></td><td><input class=\\\\\\\"num avg\\\\\\\" data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\" type=\\\\\\\"number\\\\\\\" step=\\\\\\\"any\\\\\\\" value=\\\\\\\"\\\"+inputPrice(h.avg_buy)+\\\"\\\\\\\"></td><td>\\\"+money(cost,0)+\\\"</td><td>\\\"+money(value,2)+\\\"</td><td class=\\\\\\\"\\\"+(p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+money(p,2)+\\\"</td></tr>\\\"}).join(\\\"\\\");d\"},{\"offset\":10650,\"pattern\":\"move\",\"text\":\"ested.textContent=money(inv,0);let p=total-inv;pnl.textContent=money(p,0);pnl.className=p>=0?\\\"good\\\":\\\"bad\\\";pnlp.textContent=(inv?p/inv*100:0).toFixed(2)+\\\"%\\\";day.textContent=(daily>=0?\\\"+\\\":\\\"\\\")+money(daily,2)+\\\" за 24ч\\\";updated.textContent=state.market.updated_at?new Date(state.market.updated_at*1000).toLocaleTimeString(\\\"ru-RU\\\"):\\\"нет данных\\\";marketStatus.textContent=Object.keys(m).length===2?\\\"Рынок обновлён\\\":\\\"Нет данных\\\";marketStatus.className=\\\"status \\\"+(Object.keys(m).length===2?\\\"good\\\":\\\"bad\\\");bindInputs();draw();renderMoves();renderAlerts();aCoin.innerHTML=ids.map(id=>\\\"<option value=\\\\\\\"\\\"+id+\\\"\\\\\\\">\\\"+state.coins[id]+\\\"</option>\\\").join(\\\"\\\")}function bindInputs(){document.querySelectorAll(\\\".qty,.avg\\\").forEach(x=>x.onchange=async()=>{let id=x.dataset.id;let q=document.querySelector(\\\".qty[data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"]\\\").value;let a=document.querySelector(\\\".avg[data-id=\\\\\\\"\\\"+id+\\\"\\\\\\\"]\\\").value;let avg=currency===\\\"RUB\\\"?Number(a)/fx():Number(a);await fetch(\\\"/api/holding\\\",{method:\\\"POST\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},body:JSON.stringify({coin_id:id,\"},{\"offset\":11699,\"pattern\":\"top\",\"text\":\"ity:Number(q),avg_buy:avg})});load()})}function draw(){let c=coin();chartTitle.textContent=(c.name||selected)+\\\" · \\\"+state.coins[selected]+\\\"/\\\"+currency;let a=c.sparkline_7d||[];if(a.length<2){chart.innerHTML=\\\"<text x=\\\\\\\"20\\\\\\\" y=\\\\\\\"40\\\\\\\" fill=\\\\\\\"#8fa5b6\\\\\\\">История ещё загружается</text>\\\";return}let min=Math.min(...a),max=Math.max(...a),w=900,h=270,p=18;let pts=a.map((v,i)=>(p+i*(w-2*p)/(a.length-1)).toFixed(1)+\\\",\\\"+(p+(max-v)*(h-2*p)/(max-min||1)).toFixed(1)).join(\\\" \\\");chart.innerHTML=\\\"<defs><linearGradient id=\\\\\\\"g\\\\\\\"><stop stop-color=\\\\\\\"#63e6be\\\\\\\"/><stop offset=\\\\\\\"1\\\\\\\" stop-color=\\\\\\\"#69a7ff\\\\\\\"/></linearGradient></defs><polyline fill=\\\\\\\"none\\\\\\\" stroke=\\\\\\\"url(#g)\\\\\\\" stroke-width=\\\\\\\"3\\\\\\\" points=\\\\\\\"\\\"+pts+\\\"\\\\\\\"/>\\\"}function renderMoves(){let a=Object.values(state.market.coins||{}).map(c=>({s:c.symbol,p:Number(c.change_1h||0),price:c.price})).sort((x,y)=>Math.abs(y.p)-Math.abs(x.p)).slice(0,3);moves.innerHTML=a.map(x=>\\\"<div class=\\\\\\\"move\\\\\\\"><span class=\\\\\\\"muted\\\\\\\">Изменение за 1 час</span><b class=\\\\\\\"\\\"+(x.p>=0?\\\"good\\\":\\\"bad\\\")+\\\"\\\\\\\">\\\"+x.s+\\\" \\\"+(x.p>=0?\\\"+\\\":\\\"\\\")+x.p.toFixe\"},{\"offset\":12994,\"pattern\":\"alert\",\"text\":\"nd];return c+\\\": \\\"+k+\\\" \\\"+a.threshold+(a.kind===\\\"pump\\\"||a.kind===\\\"dump\\\"?\\\"% за \\\"+Math.round(a.window_seconds/60)+\\\" мин\\\":\\\" USD\\\")}function renderAlerts(){alertList.innerHTML=state.alerts.length?state.alerts.map(a=>\\\"<div class=\\\\\\\"alert\\\\\\\"><div><b>\\\"+label(a)+\\\"</b><div class=\\\\\\\"muted\\\\\\\">\\\"+a.email+\\\"</div></div><button class=\\\\\\\"btn ghost del\\\\\\\" data-id=\\\\\\\"\\\"+a.id+\\\"\\\\\\\">Удалить</button></div>\\\").join(\\\"\\\"):\\\"<span class=\\\\\\\"muted\\\\\\\">Оповещений пока нет</span>\\\";document.querySelectorAll(\\\".del\\\").forEach(x=>x.onclick=async()=>{await fetch(\\\"/api/alert/\\\"+x.dataset.id,{method:\\\"DELETE\\\"});load()})}aKind.onchange=()=>aWindow.classList.toggle(\\\"hidden\\\",!([\\\"pump\\\",\\\"dump\\\"].includes(aKind.value)));addAlert.onclick=async()=>{let dynamic=[\\\"pump\\\",\\\"dump\\\"].includes(aKind.value);let r=await fetch(\\\"/api/alert\\\",{method:\\\"POST\\\",headers:{\\\"Content-Type\\\":\\\"application/json\\\"},body:JSON.stringify({coin_id:aCoin.value,kind:aKind.value,threshold:Number(aThreshold.value),window_seconds:dynamic?Number(aWindow.value):0,email:aEmail.value})});if(!r.ok){alert(\\\"Проверьте параметры\\\")};aThreshold.\"},{\"offset\":14116,\"pattern\":\"alert\",\"text\":\"ick=()=>{currency=x.dataset.currency;localStorage.setItem(\\\"portfolio_currency\\\",currency);render()});load();setInterval(load,60000)</script><style>/* CRYPTO_UI_V2 */.coins{grid-template-columns:repeat(2,minmax(250px,1fr))!important;gap:14px!important}.coin{min-height:120px;display:flex!important;flex-direction:column;align-items:center;justify-content:center;text-align:center}.moves{grid-template-columns:1fr!important}.move{text-align:center;min-height:72px;display:flex;flex-direction:column;justify-content:center}.alert{display:grid!important;grid-template-columns:minmax(0,1fr) minmax(170px,auto) auto!important;align-items:center!important}.sale{padding:8px 12px;border:1px solid var(--l);border-radius:10px;text-align:center;color:var(--g);font-weight:800;white-space:nowrap}@media(max-width:750px){.coins{grid-template-columns:1fr!important}.alert{grid-template-columns:1fr auto!important}.sale{grid-column:1}.del{grid-column:2;grid-row:1/3}}</style><script id=\\\"ALERT_CREATE_OVERRIDE_V3\\\"> (()=>{ const install=()=>{ const button=\"},{\"offset\":15202,\"pattern\":\"alert\",\"text\":\"const coinEl=document.getElementById(\\\"aCoin\\\"); const kindEl=document.getElementById(\\\"aKind\\\"); const thresholdEl=document.getElementById(\\\"aThreshold\\\"); const windowEl=document.getElementById(\\\"aWindow\\\"); const emailEl=document.getElementById(\\\"aEmail\\\"); if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;} button.type=\\\"button\\\"; let statusEl=document.getElementById(\\\"alertCreateStatusV3\\\"); if(!statusEl){ statusEl=document.createElement(\\\"div\\\"); statusEl.id=\\\"alertCreateStatusV3\\\"; statusEl.className=\\\"muted full\\\"; statusEl.setAttribute(\\\"aria-live\\\",\\\"polite\\\"); button.insertAdjacentElement(\\\"afterend\\\",statusEl); } const ensureOptions=()=>{ try{ if(typeof state!==\\\"undefined\\\"&&state&&state.coins){ const ids=Object.keys(state.coins); if(ids.length&&coinEl.options.length!==ids.length){ coinEl.innerHTML=ids.map(id=>'<option value=\\\"'+id+'\\\">'+state.coins[id]+'</option>').join(\\\"\\\"); } } }catch(\"}],\"relevant_classes\":[\"alerts\",\"chart\",\"coins\",\"grid4\",\"moves\",\"panel\",\"sharp-moves-panel\",\"tv-chart-error\"],\"relevant_ids\":[\"ALERT_CREATE_OVERRIDE_V3\",\"ALERT_LIST_RENDERER_V5\",\"LOCAL_TRADINGVIEW_CHARTS_V1\",\"MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1\",\"SHARP_MOVES_LAYOUT_V2\",\"TRADINGVIEW_THREE_CHARTS_STYLE_V1\",\"aCoin\",\"addAlert\",\"alertCreateStatusV3\",\"alertList\",\"chart\",\"chartTitle\",\"coins\",\"moves\",\"tvCharts\"]},\"worker\":{\"assignments\":[{\"line\":11,\"names\":[\"COINS\"],\"value\":\"{\\\"avalanche-2\\\": \\\"AVAX\\\", \\\"chainlink\\\": \\\"LINK\\\", \\\"hedera-hashgraph\\\": \\\"HBAR\\\", \\\"polygon-ecosystem-token\\\": \\\"POL\\\"}\"}],\"functions\":[{\"line\":14,\"name\":\"connect\",\"source\":\"def connect():\\n    c = sqlite3.connect(DB, timeout=15)\\n    c.row_factory = sqlite3.Row\\n    c.execute(\\\"PRAGMA journal_mode=WAL\\\")\\n    c.execute(\\\"CREATE TABLE IF NOT EXISTS holdings(coin_id TEXT PRIMARY KEY, quantity REAL NOT NULL DEFAULT 0, avg_buy REAL NOT NULL DEFAULT 0)\\\")\\n    c.execute(\\\"CREATE TABLE IF NOT EXISTS alerts(id INTEGER PRIMARY KEY AUTOINCREMENT, coin_id TEXT NOT NULL, kind TEXT NOT NULL, threshold REAL NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 0, email TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, latched INTEGER NOT NULL DEFAULT 0, last_triggered INTEGER)\\\")\\n    c.execute(\\\"CREATE TABLE IF NOT EXISTS market_history(ts INTEGER NOT NULL, coin_id TEXT NOT NULL, price REAL NOT NULL, market_cap REAL NOT NULL, volume REAL NOT NULL DEFAULT 0, PRIMARY KEY(ts,coin_id))\\\")\\n    c.execute(\\\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\\\")\\n    c.commit()\\n    return c\"},{\"line\":41,\"name\":\"fetch_market\",\"source\":\"def fetch_market():\\n    params = {\\\"vs_currency\\\":\\\"usd\\\",\\\"ids\\\":\\\",\\\".join(COINS),\\\"order\\\":\\\"market_cap_desc\\\",\\\"per_page\\\":50,\\\"page\\\":1,\\\"sparkline\\\":\\\"true\\\",\\\"price_change_percentage\\\":\\\"1h,24h,7d\\\"}\\n    with httpx.Client(timeout=25, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as client:\\n        r = client.get(\\\"https://api.coingecko.com/api/v3/coins/markets\\\", params=params)\\n        r.raise_for_status()\\n        rows = r.json()\\n    now = int(time.time())\\n    usd_rub = 0.0\\n    try:\\n        with open(MARKET, \\\"r\\\", encoding=\\\"utf-8\\\") as previous_file:\\n            previous_rate = float((json.load(previous_file) or {}).get(\\\"usd_rub\\\") or 0)\\n        if 40 <= previous_rate <= 200:\\n            usd_rub = previous_rate\\n    except Exception:\\n        pass\\n    try:\\n        with httpx.Client(timeout=15, headers={\\\"User-Agent\\\":\\\"Gram-Crypto-Portfolio/1.0\\\"}) as fx_client:\\n            fx_data = fx_client.get(\\\"https://api.coingecko.com/api/v3/simple/price\\\", params={\\\"ids\\\":\\\"bitcoin\\\",\\\"vs_currencies\\\":\\\"usd,rub\\\"}).json()[\\\"bitcoin\\\"]\\n        candidate = float(fx_data[\\\"rub\\\"]) / float(fx_data[\\\"usd\\\"])\\n        if 40 <= candidate <= 200:\\n            usd_rub = candidate\\n    except Exception:\\n        pass\\n    if not 40 <= usd_rub <= 200:\\n        usd_rub = 77.025093\\n    result = {\\\"updated_at\\\":now,\\\"currency\\\":\\\"usd\\\",\\\"usd_rub\\\":usd_rub,\\\"coins\\\":{}}\\n\\n    for x in rows:\\n        cid = str(x.get(\\\"id\\\",\\\"\\\"))\\n        if cid not in COINS:\\n            continue\\n        result[\\\"coins\\\"][cid] = {\\\"symbol\\\":COINS[cid],\\\"name\\\":x.get(\\\"name\\\"),\\\"price\\\":float(x.get(\\\"current_price\\\") or 0),\\\"market_cap\\\":float(x.get(\\\"market_cap\\\") or 0),\\\"rank\\\":x.get(\\\"market_cap_rank\\\"),\\\"volume\\\":float(x.get(\\\"total_volume\\\") or 0),\\\"high_24h\\\":x.get(\\\"high_24h\\\"),\\\"low_24h\\\":x.get(\\\"low_24h\\\"),\\\"change_1h\\\":x.get(\\\"price_change_percentage_1h_in_currency\\\"),\\\"change_24h\\\":x.get(\\\"price_change_percentage_24h_in_currency\\\"),\\\"change_7d\\\":x.get(\\\"price_change_percentage_7d_in_currency\\\"),\\\"sparkline_7d\\\":((x.get(\\\"sparkline_in_7d\\\") or {}).get(\\\"price\\\") or []),\\\"last_updated\\\":x.get(\\\"last_updated\\\")}\\n    if len(result[\\\"coins\\\"]) != len(COINS):\\n        raise RuntimeError(\\\"incomplete market response\\\")\\n    return result\"},{\"line\":119,\"name\":\"evaluate\",\"source\":\"def evaluate(c, market):\\n    now = int(time.time())\\n    triggered = 0\\n    for a in c.execute(\\\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\\\").fetchall():\\n        coin = market[\\\"coins\\\"].get(a[\\\"coin_id\\\"])\\n        if not coin:\\n            continue\\n        kind = a[\\\"kind\\\"]\\n        threshold = float(a[\\\"threshold\\\"])\\n        price = float(coin[\\\"price\\\"])\\n        cap = float(coin[\\\"market_cap\\\"])\\n        latched = int(a[\\\"latched\\\"])\\n        fired = False\\n        reset = False\\n        description = \\\"\\\"\\n        if kind == \\\"price_above\\\":\\n            fired = price >= threshold\\n            reset = price < threshold * 0.99\\n            description = \\\"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n        elif kind == \\\"price_below\\\":\\n            fired = price <= threshold\\n            reset = price > threshold * 1.01\\n            description = \\\"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],price,threshold)\\n        elif kind == \\\"cap_above\\\":\\n            fired = cap >= threshold\\n            reset = cap < threshold * 0.99\\n            description = \\\"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n        elif kind == \\\"cap_below\\\":\\n            fired = cap <= threshold\\n            reset = cap > threshold * 1.01\\n            description = \\\"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\\\".format(coin[\\\"symbol\\\"],cap,threshold)\\n        elif kind in {\\\"pump\\\",\\\"dump\\\"}:\\n            old = reference_price(c,a[\\\"coin_id\\\"],now-int(a[\\\"window_seconds\\\"]))\\n            if old and old > 0:\\n                change = (price / old - 1) * 100\\n                if kind == \\\"pump\\\":\\n                    fired = change >= threshold\\n                    reset = change < threshold * 0.5\\n                    description = \\\"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n                else:\\n                    fired = change <= -threshold\\n                    reset = change > -threshold * 0.5\\n                    description = \\\"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\\\".format(coin[\\\"symbol\\\"],change,int(a[\\\"window_seconds\\\"])//60,old,price)\\n        if reset and latched:\\n            c.execute(\\\"UPDATE alerts SET latched=0 WHERE id=?\\\",(a[\\\"id\\\"],))\\n            latched = 0\\n        if fired and not latched:\\n            subject = \\\"Gram Crypto: {}\\\".format(description.split(\\\":\\\")[0])\\n            body = description + \\\"\\\\nТекущая капитализаци\"},{\"line\":177,\"name\":\"cycle\",\"source\":\"def cycle():\\n    DATA.mkdir(parents=True, exist_ok=True)\\n    market = fetch_market()\\n    now = int(market[\\\"updated_at\\\"])\\n    with connect() as c:\\n        for cid, x in market[\\\"coins\\\"].items():\\n            c.execute(\\\"INSERT OR REPLACE INTO market_history(ts,coin_id,price,market_cap,volume) VALUES(?,?,?,?,?)\\\",(now,cid,x[\\\"price\\\"],x[\\\"market_cap\\\"],x[\\\"volume\\\"]))\\n        c.execute(\\\"DELETE FROM market_history WHERE ts<?\\\",(now-370*86400,))\\n        c.commit()\\n        triggered = evaluate(c,market)\\n        history_rows = c.execute(\\\"SELECT COUNT(*) AS n FROM market_history\\\").fetchone()[\\\"n\\\"]\\n    atomic_json(MARKET,market)\\n    atomic_json(STATUS,{\\\"status\\\":\\\"OK\\\",\\\"updated_at\\\":now,\\\"coin_count\\\":len(market[\\\"coins\\\"]),\\\"history_rows\\\":history_rows,\\\"alerts_triggered\\\":triggered})\\n    print(\\\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\\\".format(len(market[\\\"coins\\\"]),history_rows,triggered),flush=True)\"}],\"routes\":[]}}},\"command_id\":\"GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z\",\"control_plane_error\":null,\"gitea_changed\":false,\"mutation_outcome\":\"NO_MUTATION\",\"production_site_changed\":false,\"rc\":0,\"rollback_restored\":null,\"rollback_started\":false,\"schema\":1,\"status\":\"OK\"},\"rollback_restored\":null,\"rollback_started\":false,\"schema\":1,\"selected_method\":\"ALIAS_PINNED_AS_DEBIAN\",\"ssh_method_probes\":[{\"duration_seconds\":0.128,\"identity_count\":null,\"identity_sudo_read_ok\":true,\"name\":\"ALIAS_PINNED_AS_DEBIAN\",\"rc\":0,\"stderr\":\"\"},{\"duration_seconds\":0.125,\"identity_count\":2,\"identity_sudo_read_ok\":true,\"name\":\"DIRECT_PINNED_AS_DEBIAN_NO_PROXY\",\"rc\":0,\"stderr\":\"\"}],\"status\":\"OK\"}\n"
}
