CHAT_OUTPUT_BEGIN
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
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83
COMMAND_SHA256=e53767468737d29d53fc5cbdbbbd8fa6c8714ae560be2f80b167e03b95e9955a
DUPLICATE_FAILED_COMMAND_BLOCKED=false
EXECUTION_STARTED=true
CHANGE_DECLARED=false
RESULT_CONTRACT_VALID=true
RESULT_CONTRACT_STATUS=NOT_APPLICABLE
RESULT_CONTRACT_ERROR=NONE
COMMAND_RC=0
CHANGES_MADE=false
ROLLBACK_STARTED=false
ROLLBACK_RESTORED=null
MUTATION_OUTCOME=NO_MUTATION
SANITIZED=yes
SECRETS_INCLUDED=no
PRIVATE_ADDRESSES_INCLUDED=no
RAW_EVIDENCE_SHA256=d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16
SANITIZED_OUTPUT_SHA256=d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16
OUTPUT_BEGIN
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}
{"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"}

OUTPUT_END
CHAT_OUTPUT_END
