1
0
Files
homelab-public-context/runtime/latest.txt
T

35 lines
63 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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