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='=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&&tthis.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(\"\"\"Gram Crypto

Gram Crypto

https://gram1.ru/

Введите пароль для доступа к криптопортфелю.

\"\"\" + (\"Неверный пароль\" if request.query_params.get(\"bad\") else \"\") + \"\"\"
\"\"\")"},{"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'{safe_label}'\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}}
https://gram1.ru/
<"},{"offset":4993,"pattern":"chart","text":"class=\"muted\">—
Вложено
Прибыль / убыток0%
Обновление рынкакаждые 5 минут

График цены

7 дней
Загрузка

Резкие движения

Позиции портфеля

Количество и средняя цена покупки
"},{"offset":11247,"pattern":"chart","text":"\"+state.coins[id]+\"\").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=\"История ещё загружается\";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=\"x.onclick=()=>{currency=x.dataset.currency;localStorage.setItem(\"portfolio_currency\",currency);render()});load();setInterval(load,60000) "}],"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=\"История ещё загружается\";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=>\"
\"+label(a)+\"
\"+a.email+\"
\").join(\"\"):\"Оповещений пока нет\";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":" Gram Crypto