{ "schema_version": 1, "channel": "homelab-runtime", "command_id": "GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z", "status": "OK", "rc": 0, "host": "pve01", "mode": "verify", "component": "gram1-site-pol-accordion-v22-read-only-exact-diag", "started_at_utc": "2026-08-01T09:08:23Z", "finished_at_utc": "2026-08-01T09:08:24Z", "reference_register_checked": true, "reference_sha256": "5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e", "error_register_checked": true, "error_register_sha256": "24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83", "command_sha256": "e53767468737d29d53fc5cbdbbbd8fa6c8714ae560be2f80b167e03b95e9955a", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, "change_declared": false, "result_contract_valid": true, "result_contract_status": null, "result_contract_error": null, "command_rc": 0, "changes_made": false, "rollback_started": false, "rollback_restored": null, "mutation_outcome": "NO_MUTATION", "sanitized": true, "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, "raw_evidence_sha256": "d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16", "sanitized_output_sha256": "d3cd25375e3a53d9af5e39ab778e9d73467e908702a638a70e914f303c7d7f16", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", "output": "HOMELAB_RESULT_CONTRACT={\"changes_made\":false,\"command_id\":\"GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z\",\"mutation_outcome\":\"NO_MUTATION\",\"rollback_restored\":null,\"rollback_started\":false,\"status\":\"OK\",\"version\":1}\n{\"changes_made\":false,\"command_id\":\"GRAM1-SITE-POL-ACCORDION-V22-READ-ONLY-EXACT-DIAG-20260731T223749Z\",\"control_plane_error\":null,\"gitea_changed\":false,\"mutation_outcome\":\"NO_MUTATION\",\"previous_capsule_sha256\":\"72e6f01ce9e44231fd3bb6be7db3d5883c605c255f1960e7ae16af928d3d89c6\",\"previous_command_id\":\"GRAM1-SITE-AVAX-LINK-V21-READ-ONLY-RUNTIME-VERIFY-20260731T222536Z\",\"previous_output_sha256\":\"f3d82cba2fff4b3a5d8a4d0875c4131c83f92f3fcc3f9f71b0fe15616fd6a7c5\",\"previous_runner_json_sha256\":\"49eb7f6c791af47776554b7a6709ef3c84dafb8c76283db387ade4c535952b30\",\"previous_runner_text_sha256\":\"ded9edcce6da02300a64c83d16f8f3c675b4bba3ab70b8e70adc4d2394309f00\",\"production_site_changed\":false,\"rc\":0,\"remote_result\":{\"changes_made\":false,\"checks\":{\"compose_sha256\":\"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8\",\"data\":{\"files\":[{\"bytes\":20662,\"focus\":[{\"path\":\"entries.POL:1M:1000\",\"value\":\"dict\"},{\"path\":\"entries.POL:1M:1000.result.symbol\",\"value\":\"POL\"},{\"path\":\"entries.POL:1M:1000.result.pair\",\"value\":\"POLUSDT\"}],\"name\":\"klines-cache.json\",\"top_keys\":[\"entries\",\"saved_at\",\"version\"]},{\"bytes\":13860,\"focus\":[{\"path\":\"coins.chainlink.change_1h\",\"value\":\"-0.2\"},{\"path\":\"coins.chainlink.change_24h\",\"value\":\"-2.1\"},{\"path\":\"coins.chainlink.change_7d\",\"value\":\"-2.0\"},{\"path\":\"coins.hedera-hashgraph.change_1h\",\"value\":\"0.6\"},{\"path\":\"coins.hedera-hashgraph.change_24h\",\"value\":\"2.1\"},{\"path\":\"coins.hedera-hashgraph.change_7d\",\"value\":\"0.3\"},{\"path\":\"coins.avalanche-2.change_1h\",\"value\":\"-0.1\"},{\"path\":\"coins.avalanche-2.change_24h\",\"value\":\"-1.3\"},{\"path\":\"coins.avalanche-2.change_7d\",\"value\":\"2.2\"},{\"path\":\"coins.polygon-ecosystem-token\",\"value\":\"dict\"},{\"path\":\"coins.polygon-ecosystem-token.symbol\",\"value\":\"POL\"},{\"path\":\"coins.polygon-ecosystem-token.name\",\"value\":\"POL (ex-MATIC)\"},{\"path\":\"coins.polygon-ecosystem-token.change_1h\",\"value\":\"0.8\"},{\"path\":\"coins.polygon-ecosystem-token.change_24h\",\"value\":\"1.2\"},{\"path\":\"coins.polygon-ecosystem-token.change_7d\",\"value\":\"-5.9\"}],\"name\":\"market.json\",\"top_keys\":[\"coins\",\"currency\",\"updated_at\",\"usd_rub\"]},{\"bytes\":96,\"focus\":[{\"path\":\"alerts_triggered\",\"value\":\"0\"}],\"name\":\"worker-status.json\",\"top_keys\":[\"alerts_triggered\",\"coin_count\",\"history_rows\",\"status\",\"updated_at\"]}],\"now\":1785575304},\"exact_file_hashes\":{\"app.py\":\"ec226952cdb7fb58edbfc0173af47f4d901f3206e15169df12294983731fbc20\",\"templates/index.html\":\"4bff850e158898f09cbcaea38d39126c4de702f701a4f9c034d9e3a72002ff8b\",\"worker.py\":\"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c\"},\"facts\":{\"POL_in_index\":true,\"POL_in_market_data\":true,\"chart_layout_checked\":true,\"movement_section_checked\":true},\"next_decision\":\"BUILD_ONE_ATOMIC_POL_MOVEMENT_AND_ACCORDION_FIX_FROM_EXACT_CONTEXTS\",\"runtime\":{\"ports\":[8000,5000,8080,3000,80,39065],\"routes\":[{\"bytes\":1088,\"contains_POL\":false,\"content_type\":\"text/html; charset=utf-8\",\"focus\":[],\"path\":\"/login\",\"port\":8000,\"status\":200},{\"bytes\":31,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/logout\",\"port\":8000,\"status\":405},{\"bytes\":0,\"contains_POL\":false,\"content_type\":null,\"focus\":[],\"path\":\"/\",\"port\":8000,\"status\":303},{\"bytes\":66,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"market_age_seconds\",\"status\",\"type\"],\"path\":\"/health\",\"port\":8000,\"status\":200},{\"bytes\":22,\"contains_POL\":false,\"content_type\":\"application/json\",\"focus\":[],\"json_top_keys\":[\"detail\"],\"path\":\"/coin-icon/{coin_id}\",\"port\":8000,\"status\":404},{\"bytes\":196203,\"contains_POL\":false,\"content_type\":\"application/javascript\",\"focus\":[\"/*! * @license * TradingView Lightweight Charts™ v5.2.0 * Copyright (c) 2026 TradingView, Inc. * Licensed under Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0 */ !function(){\\\"use strict\\\";const t={title:\\\"\\\",visible:!0,hitTestTolerance:3,lastValueVisible:!0,priceLineVisible:!0,priceLineSource:0,priceLineWidth:1,priceLineColor:\\\"\\\",priceLineStyle:2,baseLineVisible:!0,baseLineWidth:1,baseLineColor:\\\"#B2B5BE\\\",baseLineStyle:0,priceFormat:{type:\\\"price\\\",precision:2,minMove:.01}};var i,n;function s(t,i){const n=function(t,i){switch(t){case 0:default:retu\",\"v-attr-logo{--fill:#131722;--stroke:#fff;position:absolute;left:10px;bottom:10px;height:19px;width:35px;margin:0;padding:0;border:0;z-index:3;}a#tv-attr-logo[data-dark]{--fill:#D1D4DC;--stroke:#131722;}\\\",this.Hv=document.createElement(\\\"a\\\"),this.Hv.href=`https://www.tradingview.com/?utm_medium=lwc-link&utm_campaign=lwc-chart${this.Jv()}`,this.Hv.title=\\\"Charting by TradingView\\\",this.Hv.id=\\\"tv-attr-logo\\\",this.Hv.target=\\\"_blank\\\",this.Hv.innerHTML='=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