CHAT_OUTPUT_BEGIN
COMMAND_ID=GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z
STATUS=FAIL
RC=1
HOST=pve01
MODE=verify
COMPONENT=gram1-site-usdc-v42-read-only-exact-scope
REFERENCE_REGISTER_CHECK=OK
REFERENCE_SHA256=5763f2f8edc75fcf6f3951f8c95896d1cc112ceccb6cf5b64a383f540a4deb8e
ERROR_REGISTER_CHECK=OK
ERROR_REGISTER_SHA256=24934a2c7fa5c26f6e828583c4aefac7f143b40c0171736ed7571e617d55ab83
COMMAND_SHA256=54ae5729223f6a8183e916ce1cca935d919e30ea9be4fb136555519824fedb6b
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=1
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=5f923ab9e75640a7e7fa95144ff2431810889f25caeb17a2acfaa27314ed848a
SANITIZED_OUTPUT_SHA256=5f923ab9e75640a7e7fa95144ff2431810889f25caeb17a2acfaa27314ed848a
OUTPUT_BEGIN
HOMELAB_RESULT_CONTRACT={"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z","command_rc":0,"control_plane_error":null,"mutation_outcome":"NO_MUTATION","rc":0,"remote_rc":0,"remote_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","remote_stdout_sha256":"c7b4dca0bc02e3e303214ce7ff49f252a914a1fb0db74bb0496a9e140f28cec5","report_sha256":"0791be77ab77aad39a24810e3f850c07f83f3635941b683de983e477af1ce037","rollback_restored":false,"rollback_started":false,"source_count":3,"status":"OK","version":1}
USDC_EXACT_SCOPE_REPORT_BEGIN
{"baseline_comparison":{"app.py":{"current":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","matches":true,"previous":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d"},"compose":{"current":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8","matches":true,"previous":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8"},"worker.py":{"current":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","matches":true,"previous":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c"}},"compose":{"environment_key_names":["CRYPTO_DATA_DIR","CRYPTO_EMAIL_CONFIG_FILE","CRYPTO_PASSWORD_HASH_FILE","CRYPTO_SESSION_SECRET_FILE"],"exists":true,"path":"compose.yaml","redactions":0,"runtime":{"available":true,"config_services_rc":0,"containers":[{"env_names":["CRYPTO_DATA_DIR","CRYPTO_EMAIL_CONFIG_FILE","CRYPTO_PASSWORD_HASH_FILE","CRYPTO_SESSION_SECRET_FILE","GPG_KEY","LANG","PATH","PYTHONDONTWRITEBYTECODE","PYTHONUNBUFFERED","PYTHON_SHA256","PYTHON_VERSION","TZ"],"health":null,"id":"dc202c12ba81","image":"crypto-portfolio-web","image_id":"sha256:2a9339d475b2f4a01f060162f48a23b9590eb3ba4b036b46aad72adad540c40d","mounts":[{"destination":"/data","rw":true,"source":"/opt/stacks/crypto-portfolio/data","type":"bind"},{"destination":"/run/secrets/session_secret","rw":false,"source":"/opt/stacks/crypto-portfolio/secrets/session_secret","type":"bind"},{"destination":"/run/secrets/email_config","rw":false,"source":"/opt/stacks/crypto-portfolio/secrets/email_config","type":"bind"},{"destination":"/run/secrets/password_hash","rw":false,"source":"/opt/stacks/crypto-portfolio/secrets/password_hash","type":"bind"}],"name":"crypto-portfolio","restart_count":0,"running":true,"status":"running"},{"env_names":["CRYPTO_DATA_DIR","CRYPTO_EMAIL_CONFIG_FILE","GPG_KEY","LANG","PATH","PYTHONDONTWRITEBYTECODE","PYTHONUNBUFFERED","PYTHON_SHA256","PYTHON_VERSION","TZ"],"health":null,"id":"9a2ac999b94a","image":"crypto-portfolio-worker","image_id":"sha256:7ff5fdff7ab373e6b22dfa1a677b000d83ff9d021e4464915078e506781fd0ed","mounts":[{"destination":"/data","rw":true,"source":"/opt/stacks/crypto-portfolio/data","type":"bind"},{"destination":"/run/secrets/email_config","rw":false,"source":"/opt/stacks/crypto-portfolio/secrets/email_config","type":"bind"}],"name":"crypto-portfolio-worker","restart_count":0,"running":true,"status":"running"}],"deployed_source_hashes":{"web":{"files":{"/app/app.py":{"sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","size":32843},"/app/templates/index.html":{"sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","size":38036},"/app/worker.py":{"sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","size":10300}},"rc":0,"stderr":"","stdout_sha256":"bdf20f34c34ab85a74abc35edc7698d9e914c9b292eda221abf8e8ccb37b00f1"},"worker":{"files":{"/app/app.py":{"sha256":"ec226952cdb7fb58edbfc0173af47f4d901f3206e15169df12294983731fbc20","size":18076},"/app/templates/index.html":{"sha256":"e9e723c9b916383ce10dffe8f8bbdb4049b78d89c5b139e2de5ed82194a27be4","size":36137},"/app/worker.py":{"sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","size":10300}},"rc":0,"stderr":"","stdout_sha256":"333c26b471efe160d73c01a3d842334edfb59e2d7124f3a5c6142de9ca85f9bd"}},"ps_rc":0,"services":["web","worker"]},"sanitized_sha256":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8","sha256":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8","size":1233},"identity":{"euid":0,"hostname":"core-apps","realpath":"/opt/stacks/crypto-portfolio","root":"/opt/stacks/crypto-portfolio"},"previous_verified_baseline":{"app.py":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","capsule_sha256":"ede6ac69de0b04481289b8138b3ec5a87a7dcef391a321f8624ad29d0f330956","command_id":"GRAM1-SITE-CHARTS-V40-INTERACTIVE-ACCORDION-20260801T131500Z","compose":"6d344e46b3de94ad86131a914df2ff3ff25e9d82f7ce0557cbffa468b8cdc1b8","worker.py":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c"},"schema":1,"scope_assertion":{"exclude":["sharp moves","charts","alerts"],"include":["portfolio positions","coin list","portfolio current-value total","portfolio invested total"]},"sources":[{"exists":true,"gid":0,"gzip_sha256":"e40905e690835200b2ebda03fc54e6c43aca829b08d26fe5982dc703be8c4d51","hits":{"alert":[{"line":21,"snippet":"19:    c.execute(\"PRAGMA journal_mode=WAL\")\n20:    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)\")\n21:    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)\")\n22:    c.commit()\n23:    return c"},{"line":351,"snippet":"349:    with db() as c:\n350:        holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}\n351:        alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]\n352:        history = {}\n353:        z = {r[\"name\"] for r in c.execute(\"PRAGMA table_info(market_history)\")}"},{"line":361,"snippet":"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\n360:    # CRYPTO_HISTORY_STATE_V2\n361:    return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}\n362:\n363:@app.post(\"/api/holding\")"},{"line":376,"snippet":"374:    return {\"ok\":True}\n375:\n376:@app.post(\"/api/alert\")\n377:async def add_alert(request: Request):\n378:    require_login(request)"},{"line":377,"snippet":"375:\n376:@app.post(\"/api/alert\")\n377:async def add_alert(request: Request):\n378:    require_login(request)\n379:    x = await request.json()"},{"line":387,"snippet":"385:        raise HTTPException(400,\"bad values\")\n386:    with db() as c:\n387:        cur = c.execute(\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\",(coin,kind,threshold,window,email)); c.commit()\n388:    return {\"ok\":True,\"id\":cur.lastrowid}\n389:"},{"line":390,"snippet":"388:    return {\"ok\":True,\"id\":cur.lastrowid}\n389:\n390:@app.delete(\"/api/alert/{alert_id}\")\n391:def delete_alert(alert_id: int, request: Request):\n392:    require_login(request)"},{"line":391,"snippet":"389:\n390:@app.delete(\"/api/alert/{alert_id}\")\n391:def delete_alert(alert_id: int, request: Request):\n392:    require_login(request)\n393:    with db() as c:"},{"line":394,"snippet":"392:    require_login(request)\n393:    with db() as c:\n394:        c.execute(\"DELETE FROM alerts WHERE id=?\",(alert_id,)); c.commit()\n395:    return {\"ok\":True}\n396:"}],"alerts":[{"line":21,"snippet":"19:    c.execute(\"PRAGMA journal_mode=WAL\")\n20:    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)\")\n21:    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)\")\n22:    c.commit()\n23:    return c"},{"line":351,"snippet":"349:    with db() as c:\n350:        holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}\n351:        alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]\n352:        history = {}\n353:        z = {r[\"name\"] for r in c.execute(\"PRAGMA table_info(market_history)\")}"},{"line":361,"snippet":"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\n360:    # CRYPTO_HISTORY_STATE_V2\n361:    return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}\n362:\n363:@app.post(\"/api/holding\")"},{"line":387,"snippet":"385:        raise HTTPException(400,\"bad values\")\n386:    with db() as c:\n387:        cur = c.execute(\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\",(coin,kind,threshold,window,email)); c.commit()\n388:    return {\"ok\":True,\"id\":cur.lastrowid}\n389:"},{"line":394,"snippet":"392:    require_login(request)\n393:    with db() as c:\n394:        c.execute(\"DELETE FROM alerts WHERE id=?\",(alert_id,)); c.commit()\n395:    return {\"ok\":True}\n396:"}],"chart":[{"line":138,"snippet":"136:\n137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\n138:LIGHTWEIGHT_JS = DATA / \"lightweight-charts.standalone.production.js\"\n139:KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}\n140:"},{"line":141,"snippet":"139:KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}\n140:\n141:@app.get(\"/vendor/lightweight-charts.js\")\n142:def lightweight_charts_js():\n143:    if not LIGHTWEIGHT_JS.is_file():"},{"line":142,"snippet":"140:\n141:@app.get(\"/vendor/lightweight-charts.js\")\n142:def lightweight_charts_js():\n143:    if not LIGHTWEIGHT_JS.is_file():\n144:        raise HTTPException(404, \"chart library missing\")"},{"line":144,"snippet":"142:def lightweight_charts_js():\n143:    if not LIGHTWEIGHT_JS.is_file():\n144:        raise HTTPException(404, \"chart library missing\")\n145:    return Response(LIGHTWEIGHT_JS.read_bytes(), media_type=\"application/javascript\", headers={\"Cache-Control\": \"public, max-age=31536000, immutable\"})\n146:"},{"line":312,"snippet":"310:    return fetch_klines(symbol, limit, interval)\n311:\n312:@app.get(\"/api/chart-health\")\n313:def chart_health():\n314:    now = time.time()"},{"line":313,"snippet":"311:\n312:@app.get(\"/api/chart-health\")\n313:def chart_health():\n314:    now = time.time()\n315:    details = {}"},{"line":397,"snippet":"395:    return {\"ok\":True}\n396:\n397:# GRAM1_SERVER_CHART_FALLBACK_V4\n398:import asyncio as _gram1_asyncio\n399:import base64 as _gram1_base64"},{"line":406,"snippet":"404:from fastapi.responses import Response as _Gram1Response\n405:\n406:_GRAM1_CHART_JS_B64 = \" "},{"line":407,"snippet":"405:\n406:_GRAM1_CHART_JS_B64 = \" "},{"line":409,"snippet":"407:_GRAM1_CHART_SYMBOLS = {\"HBAR\", \"POL\", \"AVAX\", \"LINK\"}\n408:\n409:@app.get(\"/vendor/gram1-charts-v4.js\", include_in_schema=False)\n410:def _gram1_chart_javascript_v4():\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})"},{"line":410,"snippet":"408:\n409:@app.get(\"/vendor/gram1-charts-v4.js\", include_in_schema=False)\n410:def _gram1_chart_javascript_v4():\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})\n412:"},{"line":411,"snippet":"409:@app.get(\"/vendor/gram1-charts-v4.js\", include_in_schema=False)\n410:def _gram1_chart_javascript_v4():\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})\n412:\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):"},{"line":413,"snippet":"411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})\n412:\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\n414:    fn = globals().get(\"klines\")\n415:    if fn is None:"},{"line":452,"snippet":"450:    return rows[-180:]\n451:\n452:def _gram1_chart_svg_v4(symbol: str, rows):\n453:    width,height,left,right,top,bottom=1200,430,72,28,24,42\n454:    lo=min(r[3] for r in rows);hi=max(r[2] for r in rows);span=max(hi-lo,abs(hi)*1e-9,1e-12)"},{"line":457,"snippet":"455:    plot_w=width-left-right;plot_h=height-top-bottom;step=plot_w/max(len(rows),1);body=max(2.0,min(10.0,step*0.58))\n456:    def y(v): return top+(hi-v)/span*plot_h\n457:    parts=[f'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" role=\"img\" aria-label=\"{_gram1_html.escape(symbol)} candle chart\" data-symbol=\"{_gram1_html.escape(symbol)}\" data-candles=\"{len(rows)}\">', '<rect width=\"100%\" height=\"100%\" fill=\"#081823\"/>']\n458:    for n in range(5):\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\"{left}\" y1=\"{yy:.2f}\" x2=\"{width-right}\" y2=\"{yy:.2f}\" stroke=\"rgba(143,165,182,.12)\"/>');parts.append(f'<text x=\"6\" y=\"{yy+4:.2f}\" fill=\"#8fa5b6\" font-size=\"12\">{price:.8g}</text>')"},{"line":466,"snippet":"464:    return ''.join(parts)\n465:\n466:@app.get(\"/api/chart-svg/{symbol}\", include_in_schema=False)\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\n468:    if not request.session.get(\"auth\"):"},{"line":467,"snippet":"465:\n466:@app.get(\"/api/chart-svg/{symbol}\", include_in_schema=False)\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\n468:    if not request.session.get(\"auth\"):\n469:        return _Gram1Response(content=\"Требуется вход\", status_code=401, media_type=\"text/plain; charset=utf-8\")"},{"line":471,"snippet":"469:        return _Gram1Response(content=\"Требуется вход\", status_code=401, media_type=\"text/plain; charset=utf-8\")\n470:    symbol=symbol.upper()\n471:    if symbol not in _GRAM1_CHART_SYMBOLS:\n472:        return _Gram1Response(content=\"Неизвестная монета\", status_code=404, media_type=\"text/plain; charset=utf-8\")\n473:    try:"},{"line":474,"snippet":"472:        return _Gram1Response(content=\"Неизвестная монета\", status_code=404, media_type=\"text/plain; charset=utf-8\")\n473:    try:\n474:        rows=await _gram1_chart_data_v4(symbol,request)\n475:        svg=_gram1_chart_svg_v4(symbol,rows)\n476:        return _Gram1Response(content=svg,media_type=\"image/svg+xml\",headers={\"Cache-Control\":\"no-store\"})"},{"line":475,"snippet":"473:    try:\n474:        rows=await _gram1_chart_data_v4(symbol,request)\n475:        svg=_gram1_chart_svg_v4(symbol,rows)\n476:        return _Gram1Response(content=svg,media_type=\"image/svg+xml\",headers={\"Cache-Control\":\"no-store\"})\n477:    except Exception as exc:"}],"charts":[{"line":138,"snippet":"136:\n137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\n138:LIGHTWEIGHT_JS = DATA / \"lightweight-charts.standalone.production.js\"\n139:KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}\n140:"},{"line":141,"snippet":"139:KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}\n140:\n141:@app.get(\"/vendor/lightweight-charts.js\")\n142:def lightweight_charts_js():\n143:    if not LIGHTWEIGHT_JS.is_file():"},{"line":142,"snippet":"140:\n141:@app.get(\"/vendor/lightweight-charts.js\")\n142:def lightweight_charts_js():\n143:    if not LIGHTWEIGHT_JS.is_file():\n144:        raise HTTPException(404, \"chart library missing\")"},{"line":409,"snippet":"407:_GRAM1_CHART_SYMBOLS = {\"HBAR\", \"POL\", \"AVAX\", \"LINK\"}\n408:\n409:@app.get(\"/vendor/gram1-charts-v4.js\", include_in_schema=False)\n410:def _gram1_chart_javascript_v4():\n411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})"}],"coin":[{"line":13,"snippet":"11:HASH_FILE = Path(os.getenv(\"CRYPTO_PASSWORD_HASH_FILE\", \"/run/secrets/password_hash\"))\n12:SECRET_FILE = Path(os.getenv(\"CRYPTO_SESSION_SECRET_FILE\", \"/run/secrets/session_secret\"))\n13:COINS = {\"hedera-hashgraph\": \"HBAR\", \"polygon-ecosystem-token\": \"POL\", \"avalanche-2\": \"AVAX\", \"chainlink\": \"LINK\"}\n14:FAILED = {}\n15:"},{"line":20,"snippet":"18:    c.row_factory = sqlite3.Row\n19:    c.execute(\"PRAGMA journal_mode=WAL\")\n20:    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)\")\n21:    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)\")\n22:    c.commit()"},{"line":21,"snippet":"19:    c.execute(\"PRAGMA journal_mode=WAL\")\n20:    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)\")\n21:    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)\")\n22:    c.commit()\n23:    return c"},{"line":91,"snippet":"89:    return {\"status\":\"OK\",\"type\":\"crypto-portfolio\",\"market_age_seconds\":age}\n90:\n91:# COIN_ICON_ROUTE_V4\n92:@app.get(\"/coin-icon/{coin_id}\")\n93:def coin_icon(coin_id: str):"},{"line":92,"snippet":"90:\n91:# COIN_ICON_ROUTE_V4\n92:@app.get(\"/coin-icon/{coin_id}\")\n93:def coin_icon(coin_id: str):\n94:    import html"},{"line":93,"snippet":"91:# COIN_ICON_ROUTE_V4\n92:@app.get(\"/coin-icon/{coin_id}\")\n93:def coin_icon(coin_id: str):\n94:    import html\n95:    import os"},{"line":101,"snippet":"99:    from fastapi import HTTPException\n100:    from fastapi.responses import Response\n101:    if not re.fullmatch(r\"[a-z0-9-]{1,64}\", coin_id):\n102:        raise HTTPException(status_code=404)\n103:    cache_dir = Path(\"/data/coin-icons\")"},{"line":103,"snippet":"101:    if not re.fullmatch(r\"[a-z0-9-]{1,64}\", coin_id):\n102:        raise HTTPException(status_code=404)\n103:    cache_dir = Path(\"/data/coin-icons\")\n104:    cache_dir.mkdir(parents=True, exist_ok=True)\n105:    body_path = cache_dir / (coin_id + \".img\")"},{"line":105,"snippet":"103:    cache_dir = Path(\"/data/coin-icons\")\n104:    cache_dir.mkdir(parents=True, exist_ok=True)\n105:    body_path = cache_dir / (coin_id + \".img\")\n106:    type_path = cache_dir / (coin_id + \".type\")\n107:    if body_path.exists() and body_path.stat().st_size > 128:"},{"line":106,"snippet":"104:    cache_dir.mkdir(parents=True, exist_ok=True)\n105:    body_path = cache_dir / (coin_id + \".img\")\n106:    type_path = cache_dir / (coin_id + \".type\")\n107:    if body_path.exists() and body_path.stat().st_size > 128:\n108:        media_type = type_path.read_text().strip() if type_path.exists() else \"image/png\""},{"line":112,"snippet":"110:    try:\n111:        with httpx.Client(timeout=6, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n112:            response = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params={\"vs_currency\":\"usd\",\"ids\":coin_id,\"sparkline\":\"false\"})\n113:            response.raise_for_status()\n114:            rows = response.json()"},{"line":128,"snippet":"126:        return Response(image.content, media_type=media_type, headers={\"Cache-Control\":\"public, max-age=86400\"})\n127:    except Exception:\n128:        symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\n130:        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]"},{"line":129,"snippet":"127:    except Exception:\n128:        symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\n130:        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]"},{"line":131,"snippet":"129:        label = symbols.get(coin_id, coin_id[:3].upper())\n130:        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]\n132:        safe_label = html.escape(label)\n133:        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>'"},{"line":350,"snippet":"348:    require_login(request)\n349:    with db() as c:\n350:        holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}\n351:        alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]\n352:        history = {}"},{"line":354,"snippet":"352:        history = {}\n353:        z = {r[\"name\"] for r in c.execute(\"PRAGMA table_info(market_history)\")}\n354:        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)\n355:        if cc and pc and tc:\n356:            q = f'SELECT \"{pc}\" p FROM market_history WHERE \"{cc}\"=? ORDER BY \"{tc}\"'"},{"line":357,"snippet":"355:        if cc and pc and tc:\n356:            q = f'SELECT \"{pc}\" p FROM market_history WHERE \"{cc}\"=? ORDER BY \"{tc}\"'\n357:            for cid in COINS:\n358:                v=[float(r[\"p\"]) for r in c.execute(q,(cid,)) if r[\"p\"] is not None]\n359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]"},{"line":361,"snippet":"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\n360:    # CRYPTO_HISTORY_STATE_V2\n361:    return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}\n362:\n363:@app.post(\"/api/holding\")"},{"line":367,"snippet":"365:    require_login(request)\n366:    x = await request.json()\n367:    coin = str(x.get(\"coin_id\",\"\"))\n368:    if coin not in COINS:\n369:        raise HTTPException(400,\"bad coin\")"},{"line":368,"snippet":"366:    x = await request.json()\n367:    coin = str(x.get(\"coin_id\",\"\"))\n368:    if coin not in COINS:\n369:        raise HTTPException(400,\"bad coin\")\n370:    quantity = max(0.0, float(x.get(\"quantity\",0)))"},{"line":369,"snippet":"367:    coin = str(x.get(\"coin_id\",\"\"))\n368:    if coin not in COINS:\n369:        raise HTTPException(400,\"bad coin\")\n370:    quantity = max(0.0, float(x.get(\"quantity\",0)))\n371:    avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))"},{"line":373,"snippet":"371:    avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))\n372:    with db() as c:\n373:        c.execute(\"INSERT INTO holdings(coin_id,quantity,avg_buy) VALUES(?,?,?) ON CONFLICT(coin_id) DO UPDATE SET quantity=excluded.quantity,avg_buy=excluded.avg_buy\",(coin,quantity,avg_buy)); c.commit()\n374:    return {\"ok\":True}\n375:"},{"line":380,"snippet":"378:    require_login(request)\n379:    x = await request.json()\n380:    coin = str(x.get(\"coin_id\",\"\")); kind = str(x.get(\"kind\",\"\"))\n381:    if coin not in COINS or kind not in {\"price_above\",\"price_below\",\"cap_above\",\"cap_below\",\"pump\",\"dump\"}:\n382:        raise HTTPException(400,\"bad rule\")"},{"line":381,"snippet":"379:    x = await request.json()\n380:    coin = str(x.get(\"coin_id\",\"\")); kind = str(x.get(\"kind\",\"\"))\n381:    if coin not in COINS or kind not in {\"price_above\",\"price_below\",\"cap_above\",\"cap_below\",\"pump\",\"dump\"}:\n382:        raise HTTPException(400,\"bad rule\")\n383:    threshold = float(x.get(\"threshold\",0)); window = int(x.get(\"window_seconds\",0)); email = str(x.get(\"email\",\"\")).strip()"},{"line":387,"snippet":"385:        raise HTTPException(400,\"bad values\")\n386:    with db() as c:\n387:        cur = c.execute(\"INSERT INTO alerts(coin_id,kind,threshold,window_seconds,email) VALUES(?,?,?,?,?)\",(coin,kind,threshold,window,email)); c.commit()\n388:    return {\"ok\":True,\"id\":cur.lastrowid}\n389:"},{"line":420,"snippet":"418:    for name, parameter in _gram1_inspect.signature(fn).parameters.items():\n419:        lname = name.lower()\n420:        if lname in {\"symbol\", \"coin\", \"ticker\"}: kwargs[name] = symbol\n421:        elif lname == \"request\": kwargs[name] = request\n422:        elif lname == \"limit\": kwargs[name] = 1000"}],"coins":[{"line":13,"snippet":"11:HASH_FILE = Path(os.getenv(\"CRYPTO_PASSWORD_HASH_FILE\", \"/run/secrets/password_hash\"))\n12:SECRET_FILE = Path(os.getenv(\"CRYPTO_SESSION_SECRET_FILE\", \"/run/secrets/session_secret\"))\n13:COINS = {\"hedera-hashgraph\": \"HBAR\", \"polygon-ecosystem-token\": \"POL\", \"avalanche-2\": \"AVAX\", \"chainlink\": \"LINK\"}\n14:FAILED = {}\n15:"},{"line":112,"snippet":"110:    try:\n111:        with httpx.Client(timeout=6, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n112:            response = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params={\"vs_currency\":\"usd\",\"ids\":coin_id,\"sparkline\":\"false\"})\n113:            response.raise_for_status()\n114:            rows = response.json()"},{"line":357,"snippet":"355:        if cc and pc and tc:\n356:            q = f'SELECT \"{pc}\" p FROM market_history WHERE \"{cc}\"=? ORDER BY \"{tc}\"'\n357:            for cid in COINS:\n358:                v=[float(r[\"p\"]) for r in c.execute(q,(cid,)) if r[\"p\"] is not None]\n359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]"},{"line":361,"snippet":"359:                history[cid]=v if len(v)<=1200 else [v[int(i*(len(v)-1)/1199)] for i in range(1200)]\n360:    # CRYPTO_HISTORY_STATE_V2\n361:    return {\"coins\":COINS,\"market\":market_data(),\"holdings\":holdings,\"alerts\":alerts,\"history\":history}\n362:\n363:@app.post(\"/api/holding\")"},{"line":368,"snippet":"366:    x = await request.json()\n367:    coin = str(x.get(\"coin_id\",\"\"))\n368:    if coin not in COINS:\n369:        raise HTTPException(400,\"bad coin\")\n370:    quantity = max(0.0, float(x.get(\"quantity\",0)))"},{"line":381,"snippet":"379:    x = await request.json()\n380:    coin = str(x.get(\"coin_id\",\"\")); kind = str(x.get(\"kind\",\"\"))\n381:    if coin not in COINS or kind not in {\"price_above\",\"price_below\",\"cap_above\",\"cap_below\",\"pump\",\"dump\"}:\n382:        raise HTTPException(400,\"bad rule\")\n383:    threshold = float(x.get(\"threshold\",0)); window = int(x.get(\"window_seconds\",0)); email = str(x.get(\"email\",\"\")).strip()"}],"portfolio":[{"line":9,"snippet":"7:\n8:DATA = Path(os.getenv(\"CRYPTO_DATA_DIR\", \"/data\"))\n9:DB = DATA / \"portfolio.db\"\n10:MARKET = DATA / \"market.json\"\n11:HASH_FILE = Path(os.getenv(\"CRYPTO_PASSWORD_HASH_FILE\", \"/run/secrets/password_hash\"))"},{"line":89,"snippet":"87:    if MARKET.exists():\n88:        age = int(time.time() - MARKET.stat().st_mtime)\n89:    return {\"status\":\"OK\",\"type\":\"crypto-portfolio\",\"market_age_seconds\":age}\n90:\n91:# COIN_ICON_ROUTE_V4"},{"line":111,"snippet":"109:        return Response(body_path.read_bytes(), media_type=media_type, headers={\"Cache-Control\":\"public, max-age=86400\"})\n110:    try:\n111:        with httpx.Client(timeout=6, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n112:            response = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params={\"vs_currency\":\"usd\",\"ids\":coin_id,\"sparkline\":\"false\"})\n113:            response.raise_for_status()"},{"line":238,"snippet":"236:            request = urllib.request.Request(\n237:                \"https://data-api.binance.vision/api/v3/klines?\" + query,\n238:                headers={\"User-Agent\": \"crypto-portfolio/1.0\"},\n239:            )\n240:            rows = None"}],"price":[{"line":354,"snippet":"352:        history = {}\n353:        z = {r[\"name\"] for r in c.execute(\"PRAGMA table_info(market_history)\")}\n354:        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)\n355:        if cc and pc and tc:\n356:            q = f'SELECT \"{pc}\" p FROM market_history WHERE \"{cc}\"=? ORDER BY \"{tc}\"'"},{"line":381,"snippet":"379:    x = await request.json()\n380:    coin = str(x.get(\"coin_id\",\"\")); kind = str(x.get(\"kind\",\"\"))\n381:    if coin not in COINS or kind not in {\"price_above\",\"price_below\",\"cap_above\",\"cap_below\",\"pump\",\"dump\"}:\n382:        raise HTTPException(400,\"bad rule\")\n383:    threshold = float(x.get(\"threshold\",0)); window = int(x.get(\"window_seconds\",0)); email = str(x.get(\"email\",\"\")).strip()"},{"line":459,"snippet":"457:    parts=[f'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" role=\"img\" aria-label=\"{_gram1_html.escape(symbol)} candle chart\" data-symbol=\"{_gram1_html.escape(symbol)}\" data-candles=\"{len(rows)}\">', '<rect width=\"100%\" height=\"100%\" fill=\"#081823\"/>']\n458:    for n in range(5):\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\"{left}\" y1=\"{yy:.2f}\" x2=\"{width-right}\" y2=\"{yy:.2f}\" stroke=\"rgba(143,165,182,.12)\"/>');parts.append(f'<text x=\"6\" y=\"{yy+4:.2f}\" fill=\"#8fa5b6\" font-size=\"12\">{price:.8g}</text>')\n460:    for i,(_,o,h,l,c) in enumerate(rows):\n461:        x=left+step*(i+.5);color=\"#63e6be\" if c>=o else \"#ff6b81\";yo=y(o);yc=y(c);yh=y(h);yl=y(l);rh=max(1.2,abs(yc-yo));ry=min(yo,yc)"}],"quantity":[{"line":20,"snippet":"18:    c.row_factory = sqlite3.Row\n19:    c.execute(\"PRAGMA journal_mode=WAL\")\n20:    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)\")\n21:    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)\")\n22:    c.commit()"},{"line":350,"snippet":"348:    require_login(request)\n349:    with db() as c:\n350:        holdings = {r[\"coin_id\"]: {\"quantity\":r[\"quantity\"],\"avg_buy\":r[\"avg_buy\"]} for r in c.execute(\"SELECT * FROM holdings\")}\n351:        alerts = [dict(r) for r in c.execute(\"SELECT * FROM alerts ORDER BY id DESC\")]\n352:        history = {}"},{"line":370,"snippet":"368:    if coin not in COINS:\n369:        raise HTTPException(400,\"bad coin\")\n370:    quantity = max(0.0, float(x.get(\"quantity\",0)))\n371:    avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))\n372:    with db() as c:"},{"line":373,"snippet":"371:    avg_buy = max(0.0, float(x.get(\"avg_buy\",0)))\n372:    with db() as c:\n373:        c.execute(\"INSERT INTO holdings(coin_id,quantity,avg_buy) VALUES(?,?,?) ON CONFLICT(coin_id) DO UPDATE SET quantity=excluded.quantity,avg_buy=excluded.avg_buy\",(coin,quantity,avg_buy)); c.commit()\n374:    return {\"ok\":True}\n375:"}],"symbol":[{"line":128,"snippet":"126:        return Response(image.content, media_type=media_type, headers={\"Cache-Control\":\"public, max-age=86400\"})\n127:    except Exception:\n128:        symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\n130:        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]"},{"line":129,"snippet":"127:    except Exception:\n128:        symbols = {\"bitcoin\":\"₿\",\"ethereum\":\"Ξ\",\"solana\":\"S\",\"binancecoin\":\"BNB\",\"ripple\":\"XRP\",\"cardano\":\"ADA\",\"hedera-hashgraph\":\"HBAR\",\"hedera\":\"HBAR\",\"avalanche-2\":\"AVAX\",\"chainlink\":\"LINK\",\"toncoin\":\"TON\",\"dogecoin\":\"Ð\"}\n129:        label = symbols.get(coin_id, coin_id[:3].upper())\n130:        palette = [\"#f7931a\",\"#627eea\",\"#14f195\",\"#f3ba2f\",\"#23292f\",\"#3468d4\",\"#6f5af5\"]\n131:        color = palette[sum(ord(ch) for ch in coin_id) % len(palette)]"},{"line":139,"snippet":"137:# LOCAL_LIGHTWEIGHT_BACKEND_V1\n138:LIGHTWEIGHT_JS = DATA / \"lightweight-charts.standalone.production.js\"\n139:KLINE_SYMBOLS = {\"HBAR\": \"HBARUSDT\", \"POL\": \"POLUSDT\", \"AVAX\": \"AVAXUSDT\", \"LINK\": \"LINKUSDT\"}\n140:\n141:@app.get(\"/vendor/lightweight-charts.js\")"},{"line":171,"snippet":"169:            parts = name.split(\":\")\n170:            if len(parts) == 3:\n171:                symbol, interval, limit_text = parts\n172:            elif len(parts) == 2:\n173:                symbol, limit_text = parts"},{"line":173,"snippet":"171:                symbol, interval, limit_text = parts\n172:            elif len(parts) == 2:\n173:                symbol, limit_text = parts\n174:                interval = \"1d\"\n175:            else:"},{"line":181,"snippet":"179:            result = item.get(\"result\")\n180:            if now - saved_at <= KLINE_CACHE_MAX_STALE and _valid_kline_result(result):\n181:                KLINE_CACHE[(symbol, interval, limit)] = (saved_at, result)\n182:    except Exception:\n183:        return"},{"line":190,"snippet":"188:        for key, value in list(KLINE_CACHE.items()):\n189:            if len(key) == 3:\n190:                symbol, interval, limit = key\n191:            else:\n192:                symbol, limit = key"},{"line":192,"snippet":"190:                symbol, interval, limit = key\n191:            else:\n192:                symbol, limit = key\n193:                interval = \"1d\"\n194:            saved_at, result = value"},{"line":196,"snippet":"194:            saved_at, result = value\n195:            if _valid_kline_result(result):\n196:                entries[symbol + \":\" + interval + \":\" + str(limit)] = {\"saved_at\": saved_at, \"result\": result}\n197:        payload = {\"version\": 1, \"saved_at\": int(time.time()), \"entries\": entries}\n198:        temporary = KLINE_CACHE_FILE.with_suffix(\".json.tmp\")"},{"line":205,"snippet":"203:_load_kline_cache()\n204:\n205:def fetch_klines(symbol, limit=365, interval=\"1d\"):\n206:    key = str(symbol).upper()\n207:    pair = KLINE_SYMBOLS.get(key)"},{"line":206,"snippet":"204:\n205:def fetch_klines(symbol, limit=365, interval=\"1d\"):\n206:    key = str(symbol).upper()\n207:    pair = KLINE_SYMBOLS.get(key)\n208:    if not pair:"},{"line":207,"snippet":"205:def fetch_klines(symbol, limit=365, interval=\"1d\"):\n206:    key = str(symbol).upper()\n207:    pair = KLINE_SYMBOLS.get(key)\n208:    if not pair:\n209:        raise HTTPException(404, \"unknown symbol\")"},{"line":209,"snippet":"207:    pair = KLINE_SYMBOLS.get(key)\n208:    if not pair:\n209:        raise HTTPException(404, \"unknown symbol\")\n210:    interval = str(interval or \"1d\")\n211:    if interval not in {\"1d\", \"1M\"}:"},{"line":232,"snippet":"230:        while remaining > 0:\n231:            page_limit = min(1000 if interval == \"1M\" else 100, remaining)\n232:            params = {\"symbol\": pair, \"interval\": interval, \"limit\": page_limit}\n233:            if end_time is not None:\n234:                params[\"endTime\"] = end_time"},{"line":295,"snippet":"293:    result = {\n294:        \"status\": \"OK\",\n295:        \"symbol\": key,\n296:        \"pair\": pair,\n297:        \"candles\": candles,"},{"line":307,"snippet":"305:    return dict(result)\n306:\n307:@app.get(\"/api/klines/{symbol}\")\n308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \"1d\"):\n309:    require_login(request)"},{"line":308,"snippet":"306:\n307:@app.get(\"/api/klines/{symbol}\")\n308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \"1d\"):\n309:    require_login(request)\n310:    return fetch_klines(symbol, limit, interval)"},{"line":310,"snippet":"308:def klines(symbol: str, request: Request, limit: int = 365, interval: str = \"1d\"):\n309:    require_login(request)\n310:    return fetch_klines(symbol, limit, interval)\n311:\n312:@app.get(\"/api/chart-health\")"},{"line":317,"snippet":"315:    details = {}\n316:    bad = 0\n317:    for symbol in (\"HBAR\", \"POL\", \"AVAX\", \"LINK\"):\n318:        entry = KLINE_CACHE.get((symbol, \"1M\", 1000)) or KLINE_CACHE.get((symbol, \"1d\", 365))\n319:        if not entry or not _valid_kline_result(entry[1]):"},{"line":318,"snippet":"316:    bad = 0\n317:    for symbol in (\"HBAR\", \"POL\", \"AVAX\", \"LINK\"):\n318:        entry = KLINE_CACHE.get((symbol, \"1M\", 1000)) or KLINE_CACHE.get((symbol, \"1d\", 365))\n319:        if not entry or not _valid_kline_result(entry[1]):\n320:            details[symbol] = {\"status\": \"ERROR\", \"candles\": 0, \"cache_age_seconds\": None}"},{"line":320,"snippet":"318:        entry = KLINE_CACHE.get((symbol, \"1M\", 1000)) or KLINE_CACHE.get((symbol, \"1d\", 365))\n319:        if not entry or not _valid_kline_result(entry[1]):\n320:            details[symbol] = {\"status\": \"ERROR\", \"candles\": 0, \"cache_age_seconds\": None}\n321:            bad += 1\n322:            continue"},{"line":325,"snippet":"323:        age = int(now - entry[0])\n324:        expired = age > KLINE_CACHE_MAX_STALE\n325:        details[symbol] = {\n326:            \"status\": \"ERROR\" if expired else \"OK\",\n327:            \"candles\": len(entry[1][\"candles\"]),"},{"line":341,"snippet":"339:        \"library_ok\": library_ok,\n340:        \"disk_cache_present\": KLINE_CACHE_FILE.is_file(),\n341:        \"symbols\": details,\n342:        \"bad\": bad,\n343:    }"},{"line":407,"snippet":"405:\n406:_GRAM1_CHART_JS_B64 = \" "},{"line":413,"snippet":"411:    return _Gram1Response(content=_gram1_base64.b64decode(_GRAM1_CHART_JS_B64), media_type=\"application/javascript\", headers={\"Cache-Control\":\"no-store\"})\n412:\n413:async def _gram1_chart_data_v4(symbol: str, request: _Gram1Request):\n414:    fn = globals().get(\"klines\")\n415:    if fn is None:"},{"line":420,"snippet":"418:    for name, parameter in _gram1_inspect.signature(fn).parameters.items():\n419:        lname = name.lower()\n420:        if lname in {\"symbol\", \"coin\", \"ticker\"}: kwargs[name] = symbol\n421:        elif lname == \"request\": kwargs[name] = request\n422:        elif lname == \"limit\": kwargs[name] = 1000"},{"line":452,"snippet":"450:    return rows[-180:]\n451:\n452:def _gram1_chart_svg_v4(symbol: str, rows):\n453:    width,height,left,right,top,bottom=1200,430,72,28,24,42\n454:    lo=min(r[3] for r in rows);hi=max(r[2] for r in rows);span=max(hi-lo,abs(hi)*1e-9,1e-12)"},{"line":457,"snippet":"455:    plot_w=width-left-right;plot_h=height-top-bottom;step=plot_w/max(len(rows),1);body=max(2.0,min(10.0,step*0.58))\n456:    def y(v): return top+(hi-v)/span*plot_h\n457:    parts=[f'<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {width} {height}\" role=\"img\" aria-label=\"{_gram1_html.escape(symbol)} candle chart\" data-symbol=\"{_gram1_html.escape(symbol)}\" data-candles=\"{len(rows)}\">', '<rect width=\"100%\" height=\"100%\" fill=\"#081823\"/>']\n458:    for n in range(5):\n459:        yy=top+plot_h*n/4;price=hi-span*n/4;parts.append(f'<line x1=\"{left}\" y1=\"{yy:.2f}\" x2=\"{width-right}\" y2=\"{yy:.2f}\" stroke=\"rgba(143,165,182,.12)\"/>');parts.append(f'<text x=\"6\" y=\"{yy+4:.2f}\" fill=\"#8fa5b6\" font-size=\"12\">{price:.8g}</text>')"},{"line":463,"snippet":"461:        x=left+step*(i+.5);color=\"#63e6be\" if c>=o else \"#ff6b81\";yo=y(o);yc=y(c);yh=y(h);yl=y(l);rh=max(1.2,abs(yc-yo));ry=min(yo,yc)\n462:        parts.append(f'<line x1=\"{x:.2f}\" y1=\"{yh:.2f}\" x2=\"{x:.2f}\" y2=\"{yl:.2f}\" stroke=\"{color}\" stroke-width=\"1.2\"/>');parts.append(f'<rect x=\"{x-body/2:.2f}\" y=\"{ry:.2f}\" width=\"{body:.2f}\" height=\"{rh:.2f}\" fill=\"{color}\"/>')\n463:    parts.append(f'<text x=\"{left}\" y=\"{height-12}\" fill=\"#8fa5b6\" font-size=\"13\">{_gram1_html.escape(symbol)} / USDT · 1M · {len(rows)} свечей</text></svg>')\n464:    return ''.join(parts)\n465:"},{"line":466,"snippet":"464:    return ''.join(parts)\n465:\n466:@app.get(\"/api/chart-svg/{symbol}\", include_in_schema=False)\n467:async def _gram1_chart_svg_endpoint_v4(symbol: str, request: _Gram1Request):\n468:    if not request.session.get(\"auth\"):"}]},"line_count":481,"mode":"0o640","path":"app.py","redactions":1,"sanitized_sha256":"b6105195f16c4efb725a7629fd2d485d2c97cfab21fcb94344925d1a2dcf8bb2","sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","size":32843,"structure":{"defs":[{"end_line":23,"kind":"FunctionDef","line":16,"name":"db"},{"end_line":26,"kind":"FunctionDef","line":25,"name":"b64d"},{"end_line":36,"kind":"FunctionDef","line":28,"name":"verify_password"},{"end_line":40,"kind":"FunctionDef","line":38,"name":"require_login"},{"end_line":47,"kind":"FunctionDef","line":42,"name":"market_data"},{"end_line":56,"kind":"FunctionDef","line":53,"name":"login_page"},{"end_line":71,"kind":"FunctionDef","line":59,"name":"login"},{"end_line":76,"kind":"FunctionDef","line":74,"name":"logout"},{"end_line":82,"kind":"FunctionDef","line":79,"name":"home"},{"end_line":89,"kind":"FunctionDef","line":85,"name":"health"},{"end_line":134,"kind":"FunctionDef","line":93,"name":"coin_icon"},{"end_line":145,"kind":"FunctionDef","line":142,"name":"lightweight_charts_js"},{"end_line":159,"kind":"FunctionDef","line":153,"name":"_valid_kline_result"},{"end_line":183,"kind":"FunctionDef","line":161,"name":"_load_kline_cache"},{"end_line":201,"kind":"FunctionDef","line":185,"name":"_save_kline_cache"},{"end_line":305,"kind":"FunctionDef","line":205,"name":"fetch_klines"},{"end_line":310,"kind":"FunctionDef","line":308,"name":"klines"},{"end_line":344,"kind":"FunctionDef","line":313,"name":"chart_health"},{"end_line":361,"kind":"FunctionDef","line":347,"name":"state"},{"end_line":374,"kind":"AsyncFunctionDef","line":364,"name":"save_holding"},{"end_line":388,"kind":"AsyncFunctionDef","line":377,"name":"add_alert"},{"end_line":395,"kind":"FunctionDef","line":391,"name":"delete_alert"},{"end_line":411,"kind":"FunctionDef","line":410,"name":"_gram1_chart_javascript_v4"},{"end_line":450,"kind":"AsyncFunctionDef","line":413,"name":"_gram1_chart_data_v4"},{"end_line":464,"kind":"FunctionDef","line":452,"name":"_gram1_chart_svg_v4"},{"end_line":480,"kind":"AsyncFunctionDef","line":467,"name":"_gram1_chart_svg_endpoint_v4"}],"parse_ok":true},"uid":0},{"exists":true,"gid":0,"gzip_sha256":"49befa11ea71cd7a4c7d0a41e10d092232ecf6a2438c3311495ea36b7dcc1cda","hits":{"alert":[{"line":19,"snippet":"17:    c.execute(\"PRAGMA journal_mode=WAL\")\n18:    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)\")\n19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")"},{"line":122,"snippet":"120:    now = int(time.time())\n121:    triggered = 0\n122:    for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n123:        coin = market[\"coins\"].get(a[\"coin_id\"])\n124:        if not coin:"},{"line":163,"snippet":"161:                    description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n162:        if reset and latched:\n163:            c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\",(a[\"id\"],))\n164:            latched = 0\n165:        if fired and not latched:"},{"line":170,"snippet":"168:            ok, result = send_email(a[\"email\"],subject,body)\n169:            if ok:\n170:                c.execute(\"UPDATE alerts SET latched=1,last_triggered=? WHERE id=?\",(now,a[\"id\"]))\n171:                triggered += 1\n172:            else:"},{"line":173,"snippet":"171:                triggered += 1\n172:            else:\n173:                print(\"ALERT_EMAIL_RESULT={} ALERT_ID={}\".format(result,a[\"id\"]),flush=True)\n174:    c.commit()\n175:    return triggered"},{"line":189,"snippet":"187:        history_rows = c.execute(\"SELECT COUNT(*) AS n FROM market_history\").fetchone()[\"n\"]\n188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:"},{"line":190,"snippet":"188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:\n192:def main():"}],"alerts":[{"line":19,"snippet":"17:    c.execute(\"PRAGMA journal_mode=WAL\")\n18:    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)\")\n19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")"},{"line":122,"snippet":"120:    now = int(time.time())\n121:    triggered = 0\n122:    for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n123:        coin = market[\"coins\"].get(a[\"coin_id\"])\n124:        if not coin:"},{"line":163,"snippet":"161:                    description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n162:        if reset and latched:\n163:            c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\",(a[\"id\"],))\n164:            latched = 0\n165:        if fired and not latched:"},{"line":170,"snippet":"168:            ok, result = send_email(a[\"email\"],subject,body)\n169:            if ok:\n170:                c.execute(\"UPDATE alerts SET latched=1,last_triggered=? WHERE id=?\",(now,a[\"id\"]))\n171:                triggered += 1\n172:            else:"},{"line":189,"snippet":"187:        history_rows = c.execute(\"SELECT COUNT(*) AS n FROM market_history\").fetchone()[\"n\"]\n188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:"},{"line":190,"snippet":"188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:\n192:def main():"}],"coin":[{"line":11,"snippet":"9:STATUS = DATA / \"worker-status.json\"\n10:EMAIL_CONFIG = Path(os.getenv(\"CRYPTO_EMAIL_CONFIG_FILE\", \"/run/secrets/email_config\"))\n11:COINS = {\"hedera-hashgraph\": \"HBAR\", \"polygon-ecosystem-token\": \"POL\", \"avalanche-2\": \"AVAX\", \"chainlink\": \"LINK\"}\n12:INTERVAL = 300\n13:"},{"line":18,"snippet":"16:    c.row_factory = sqlite3.Row\n17:    c.execute(\"PRAGMA journal_mode=WAL\")\n18:    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)\")\n19:    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)\")\n20:    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))\")"},{"line":19,"snippet":"17:    c.execute(\"PRAGMA journal_mode=WAL\")\n18:    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)\")\n19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")"},{"line":20,"snippet":"18:    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)\")\n19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")\n22:    c.commit()"},{"line":21,"snippet":"19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")\n22:    c.commit()\n23:    return c"},{"line":42,"snippet":"40:\n41:def fetch_market():\n42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)"},{"line":44,"snippet":"42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)\n45:        r.raise_for_status()\n46:        rows = r.json()"},{"line":58,"snippet":"56:    try:\n57:        with httpx.Client(timeout=15, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as fx_client:\n58:            fx_data = fx_client.get(\"https://api.coingecko.com/api/v3/simple/price\", params={\"ids\":\"bitcoin\",\"vs_currencies\":\"usd,rub\"}).json()[\"bitcoin\"]\n59:        candidate = float(fx_data[\"rub\"]) / float(fx_data[\"usd\"])\n60:        if 40 <= candidate <= 200:"},{"line":66,"snippet":"64:    if not 40 <= usd_rub <= 200:\n65:        usd_rub = 77.025093\n66:    result = {\"updated_at\":now,\"currency\":\"usd\",\"usd_rub\":usd_rub,\"coins\":{}}\n67:\n68:    for x in rows:"},{"line":70,"snippet":"68:    for x in rows:\n69:        cid = str(x.get(\"id\",\"\"))\n70:        if cid not in COINS:\n71:            continue\n72:        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\")}"},{"line":72,"snippet":"70:        if cid not in COINS:\n71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")"},{"line":73,"snippet":"71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")\n75:    return result"},{"line":77,"snippet":"75:    return result\n76:\n77:def reference_price(c, coin_id, target_ts):\n78:    r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n79:    return float(r[\"price\"]) if r else None"},{"line":78,"snippet":"76:\n77:def reference_price(c, coin_id, target_ts):\n78:    r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n79:    return float(r[\"price\"]) if r else None\n80:"},{"line":107,"snippet":"105:        return False, type(e).__name__\n106:\n107:def portfolio_text(c, coin_id, price):\n108:    r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n109:    if not r:"},{"line":108,"snippet":"106:\n107:def portfolio_text(c, coin_id, price):\n108:    r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n109:    if not r:\n110:        return \"\""},{"line":123,"snippet":"121:    triggered = 0\n122:    for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n123:        coin = market[\"coins\"].get(a[\"coin_id\"])\n124:        if not coin:\n125:            continue"},{"line":124,"snippet":"122:    for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n123:        coin = market[\"coins\"].get(a[\"coin_id\"])\n124:        if not coin:\n125:            continue\n126:        kind = a[\"kind\"]"},{"line":128,"snippet":"126:        kind = a[\"kind\"]\n127:        threshold = float(a[\"threshold\"])\n128:        price = float(coin[\"price\"])\n129:        cap = float(coin[\"market_cap\"])\n130:        latched = int(a[\"latched\"])"},{"line":129,"snippet":"127:        threshold = float(a[\"threshold\"])\n128:        price = float(coin[\"price\"])\n129:        cap = float(coin[\"market_cap\"])\n130:        latched = int(a[\"latched\"])\n131:        fired = False"},{"line":137,"snippet":"135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":\n139:            fired = price <= threshold"},{"line":141,"snippet":"139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01\n141:            description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n142:        elif kind == \"cap_above\":\n143:            fired = cap >= threshold"},{"line":145,"snippet":"143:            fired = cap >= threshold\n144:            reset = cap < threshold * 0.99\n145:            description = \"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n146:        elif kind == \"cap_below\":\n147:            fired = cap <= threshold"},{"line":149,"snippet":"147:            fired = cap <= threshold\n148:            reset = cap > threshold * 1.01\n149:            description = \"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n150:        elif kind in {\"pump\",\"dump\"}:\n151:            old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))"},{"line":151,"snippet":"149:            description = \"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n150:        elif kind in {\"pump\",\"dump\"}:\n151:            old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))\n152:            if old and old > 0:\n153:                change = (price / old - 1) * 100"},{"line":157,"snippet":"155:                    fired = change >= threshold\n156:                    reset = change < threshold * 0.5\n157:                    description = \"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n158:                else:\n159:                    fired = change <= -threshold"},{"line":161,"snippet":"159:                    fired = change <= -threshold\n160:                    reset = change > -threshold * 0.5\n161:                    description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n162:        if reset and latched:\n163:            c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\",(a[\"id\"],))"},{"line":167,"snippet":"165:        if fired and not latched:\n166:            subject = \"Gram Crypto: {}\".format(description.split(\":\")[0])\n167:            body = description + \"\\nТекущая капитализация: ${:,.0f}\\n\".format(cap) + portfolio_text(c,a[\"coin_id\"],price) + \"\\nВремя события: \" + time.strftime(\"%Y-%m-%d %H:%M:%S %Z\")\n168:            ok, result = send_email(a[\"email\"],subject,body)\n169:            if ok:"},{"line":182,"snippet":"180:    now = int(market[\"updated_at\"])\n181:    with connect() as c:\n182:        for cid, x in market[\"coins\"].items():\n183:            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\"]))\n184:        c.execute(\"DELETE FROM market_history WHERE ts<?\",(now-370*86400,))"},{"line":183,"snippet":"181:    with connect() as c:\n182:        for cid, x in market[\"coins\"].items():\n183:            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\"]))\n184:        c.execute(\"DELETE FROM market_history WHERE ts<?\",(now-370*86400,))\n185:        c.commit()"}],"coins":[{"line":11,"snippet":"9:STATUS = DATA / \"worker-status.json\"\n10:EMAIL_CONFIG = Path(os.getenv(\"CRYPTO_EMAIL_CONFIG_FILE\", \"/run/secrets/email_config\"))\n11:COINS = {\"hedera-hashgraph\": \"HBAR\", \"polygon-ecosystem-token\": \"POL\", \"avalanche-2\": \"AVAX\", \"chainlink\": \"LINK\"}\n12:INTERVAL = 300\n13:"},{"line":42,"snippet":"40:\n41:def fetch_market():\n42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)"},{"line":44,"snippet":"42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)\n45:        r.raise_for_status()\n46:        rows = r.json()"},{"line":66,"snippet":"64:    if not 40 <= usd_rub <= 200:\n65:        usd_rub = 77.025093\n66:    result = {\"updated_at\":now,\"currency\":\"usd\",\"usd_rub\":usd_rub,\"coins\":{}}\n67:\n68:    for x in rows:"},{"line":70,"snippet":"68:    for x in rows:\n69:        cid = str(x.get(\"id\",\"\"))\n70:        if cid not in COINS:\n71:            continue\n72:        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\")}"},{"line":72,"snippet":"70:        if cid not in COINS:\n71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")"},{"line":73,"snippet":"71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")\n75:    return result"},{"line":123,"snippet":"121:    triggered = 0\n122:    for a in c.execute(\"SELECT * FROM alerts WHERE enabled=1 ORDER BY id\").fetchall():\n123:        coin = market[\"coins\"].get(a[\"coin_id\"])\n124:        if not coin:\n125:            continue"},{"line":182,"snippet":"180:    now = int(market[\"updated_at\"])\n181:    with connect() as c:\n182:        for cid, x in market[\"coins\"].items():\n183:            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\"]))\n184:        c.execute(\"DELETE FROM market_history WHERE ts<?\",(now-370*86400,))"},{"line":189,"snippet":"187:        history_rows = c.execute(\"SELECT COUNT(*) AS n FROM market_history\").fetchone()[\"n\"]\n188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:"},{"line":190,"snippet":"188:    atomic_json(MARKET,market)\n189:    atomic_json(STATUS,{\"status\":\"OK\",\"updated_at\":now,\"coin_count\":len(market[\"coins\"]),\"history_rows\":history_rows,\"alerts_triggered\":triggered})\n190:    print(\"WORKER_CYCLE=OK COINS={} HISTORY_ROWS={} ALERTS_TRIGGERED={}\".format(len(market[\"coins\"]),history_rows,triggered),flush=True)\n191:\n192:def main():"}],"invested":[{"line":114,"snippet":"112:    avg = float(r[\"avg_buy\"])\n113:    value = quantity * price\n114:    invested = quantity * avg\n115:    pnl = value - invested\n116:    pct = pnl / invested * 100 if invested else 0"},{"line":115,"snippet":"113:    value = quantity * price\n114:    invested = quantity * avg\n115:    pnl = value - invested\n116:    pct = pnl / invested * 100 if invested else 0\n117:    return \"\\nКоличество монет: {:.8f}\\nТекущая стоимость позиции: ${:,.2f}\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\n\".format(quantity,value,pnl,pct)"},{"line":116,"snippet":"114:    invested = quantity * avg\n115:    pnl = value - invested\n116:    pct = pnl / invested * 100 if invested else 0\n117:    return \"\\nКоличество монет: {:.8f}\\nТекущая стоимость позиции: ${:,.2f}\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\n\".format(quantity,value,pnl,pct)\n118:"}],"portfolio":[{"line":7,"snippet":"5:\n6:DATA = Path(os.getenv(\"CRYPTO_DATA_DIR\", \"/data\"))\n7:DB = DATA / \"portfolio.db\"\n8:MARKET = DATA / \"market.json\"\n9:STATUS = DATA / \"worker-status.json\""},{"line":43,"snippet":"41:def fetch_market():\n42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)\n45:        r.raise_for_status()"},{"line":57,"snippet":"55:        pass\n56:    try:\n57:        with httpx.Client(timeout=15, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as fx_client:\n58:            fx_data = fx_client.get(\"https://api.coingecko.com/api/v3/simple/price\", params={\"ids\":\"bitcoin\",\"vs_currencies\":\"usd,rub\"}).json()[\"bitcoin\"]\n59:        candidate = float(fx_data[\"rub\"]) / float(fx_data[\"usd\"])"},{"line":107,"snippet":"105:        return False, type(e).__name__\n106:\n107:def portfolio_text(c, coin_id, price):\n108:    r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n109:    if not r:"},{"line":167,"snippet":"165:        if fired and not latched:\n166:            subject = \"Gram Crypto: {}\".format(description.split(\":\")[0])\n167:            body = description + \"\\nТекущая капитализация: ${:,.0f}\\n\".format(cap) + portfolio_text(c,a[\"coin_id\"],price) + \"\\nВремя события: \" + time.strftime(\"%Y-%m-%d %H:%M:%S %Z\")\n168:            ok, result = send_email(a[\"email\"],subject,body)\n169:            if ok:"}],"price":[{"line":20,"snippet":"18:    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)\")\n19:    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)\")\n20:    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))\")\n21:    c.execute(\"CREATE INDEX IF NOT EXISTS idx_market_history_coin_ts ON market_history(coin_id,ts)\")\n22:    c.commit()"},{"line":42,"snippet":"40:\n41:def fetch_market():\n42:    params = {\"vs_currency\":\"usd\",\"ids\":\",\".join(COINS),\"order\":\"market_cap_desc\",\"per_page\":50,\"page\":1,\"sparkline\":\"true\",\"price_change_percentage\":\"1h,24h,7d\"}\n43:    with httpx.Client(timeout=25, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as client:\n44:        r = client.get(\"https://api.coingecko.com/api/v3/coins/markets\", params=params)"},{"line":58,"snippet":"56:    try:\n57:        with httpx.Client(timeout=15, headers={\"User-Agent\":\"Gram-Crypto-Portfolio/1.0\"}) as fx_client:\n58:            fx_data = fx_client.get(\"https://api.coingecko.com/api/v3/simple/price\", params={\"ids\":\"bitcoin\",\"vs_currencies\":\"usd,rub\"}).json()[\"bitcoin\"]\n59:        candidate = float(fx_data[\"rub\"]) / float(fx_data[\"usd\"])\n60:        if 40 <= candidate <= 200:"},{"line":72,"snippet":"70:        if cid not in COINS:\n71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")"},{"line":77,"snippet":"75:    return result\n76:\n77:def reference_price(c, coin_id, target_ts):\n78:    r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n79:    return float(r[\"price\"]) if r else None"},{"line":78,"snippet":"76:\n77:def reference_price(c, coin_id, target_ts):\n78:    r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n79:    return float(r[\"price\"]) if r else None\n80:"},{"line":79,"snippet":"77:def reference_price(c, coin_id, target_ts):\n78:    r = c.execute(\"SELECT price FROM market_history WHERE coin_id=? AND ts<=? ORDER BY ts DESC LIMIT 1\",(coin_id,target_ts)).fetchone()\n79:    return float(r[\"price\"]) if r else None\n80:\n81:def send_email(recipient, subject, body):"},{"line":107,"snippet":"105:        return False, type(e).__name__\n106:\n107:def portfolio_text(c, coin_id, price):\n108:    r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n109:    if not r:"},{"line":113,"snippet":"111:    quantity = float(r[\"quantity\"])\n112:    avg = float(r[\"avg_buy\"])\n113:    value = quantity * price\n114:    invested = quantity * avg\n115:    pnl = value - invested"},{"line":128,"snippet":"126:        kind = a[\"kind\"]\n127:        threshold = float(a[\"threshold\"])\n128:        price = float(coin[\"price\"])\n129:        cap = float(coin[\"market_cap\"])\n130:        latched = int(a[\"latched\"])"},{"line":134,"snippet":"132:        reset = False\n133:        description = \"\"\n134:        if kind == \"price_above\":\n135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99"},{"line":135,"snippet":"133:        description = \"\"\n134:        if kind == \"price_above\":\n135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)"},{"line":136,"snippet":"134:        if kind == \"price_above\":\n135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":"},{"line":137,"snippet":"135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":\n139:            fired = price <= threshold"},{"line":138,"snippet":"136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":\n139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01"},{"line":139,"snippet":"137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":\n139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01\n141:            description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)"},{"line":140,"snippet":"138:        elif kind == \"price_below\":\n139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01\n141:            description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n142:        elif kind == \"cap_above\":"},{"line":141,"snippet":"139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01\n141:            description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n142:        elif kind == \"cap_above\":\n143:            fired = cap >= threshold"},{"line":151,"snippet":"149:            description = \"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n150:        elif kind in {\"pump\",\"dump\"}:\n151:            old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))\n152:            if old and old > 0:\n153:                change = (price / old - 1) * 100"},{"line":153,"snippet":"151:            old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))\n152:            if old and old > 0:\n153:                change = (price / old - 1) * 100\n154:                if kind == \"pump\":\n155:                    fired = change >= threshold"},{"line":157,"snippet":"155:                    fired = change >= threshold\n156:                    reset = change < threshold * 0.5\n157:                    description = \"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n158:                else:\n159:                    fired = change <= -threshold"},{"line":161,"snippet":"159:                    fired = change <= -threshold\n160:                    reset = change > -threshold * 0.5\n161:                    description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n162:        if reset and latched:\n163:            c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\",(a[\"id\"],))"},{"line":167,"snippet":"165:        if fired and not latched:\n166:            subject = \"Gram Crypto: {}\".format(description.split(\":\")[0])\n167:            body = description + \"\\nТекущая капитализация: ${:,.0f}\\n\".format(cap) + portfolio_text(c,a[\"coin_id\"],price) + \"\\nВремя события: \" + time.strftime(\"%Y-%m-%d %H:%M:%S %Z\")\n168:            ok, result = send_email(a[\"email\"],subject,body)\n169:            if ok:"},{"line":183,"snippet":"181:    with connect() as c:\n182:        for cid, x in market[\"coins\"].items():\n183:            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\"]))\n184:        c.execute(\"DELETE FROM market_history WHERE ts<?\",(now-370*86400,))\n185:        c.commit()"}],"quantity":[{"line":18,"snippet":"16:    c.row_factory = sqlite3.Row\n17:    c.execute(\"PRAGMA journal_mode=WAL\")\n18:    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)\")\n19:    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)\")\n20:    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))\")"},{"line":108,"snippet":"106:\n107:def portfolio_text(c, coin_id, price):\n108:    r = c.execute(\"SELECT quantity,avg_buy FROM holdings WHERE coin_id=?\",(coin_id,)).fetchone()\n109:    if not r:\n110:        return \"\""},{"line":111,"snippet":"109:    if not r:\n110:        return \"\"\n111:    quantity = float(r[\"quantity\"])\n112:    avg = float(r[\"avg_buy\"])\n113:    value = quantity * price"},{"line":113,"snippet":"111:    quantity = float(r[\"quantity\"])\n112:    avg = float(r[\"avg_buy\"])\n113:    value = quantity * price\n114:    invested = quantity * avg\n115:    pnl = value - invested"},{"line":114,"snippet":"112:    avg = float(r[\"avg_buy\"])\n113:    value = quantity * price\n114:    invested = quantity * avg\n115:    pnl = value - invested\n116:    pct = pnl / invested * 100 if invested else 0"},{"line":117,"snippet":"115:    pnl = value - invested\n116:    pct = pnl / invested * 100 if invested else 0\n117:    return \"\\nКоличество монет: {:.8f}\\nТекущая стоимость позиции: ${:,.2f}\\nПрибыль или убыток: ${:,.2f} ({:+.2f}%)\\n\".format(quantity,value,pnl,pct)\n118:\n119:def evaluate(c, market):"}],"symbol":[{"line":72,"snippet":"70:        if cid not in COINS:\n71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")"},{"line":137,"snippet":"135:            fired = price >= threshold\n136:            reset = price < threshold * 0.99\n137:            description = \"Цена {} достигла ${:,.8f}, порог выше или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n138:        elif kind == \"price_below\":\n139:            fired = price <= threshold"},{"line":141,"snippet":"139:            fired = price <= threshold\n140:            reset = price > threshold * 1.01\n141:            description = \"Цена {} снизилась до ${:,.8f}, порог ниже или равно ${:,.8f}.\".format(coin[\"symbol\"],price,threshold)\n142:        elif kind == \"cap_above\":\n143:            fired = cap >= threshold"},{"line":145,"snippet":"143:            fired = cap >= threshold\n144:            reset = cap < threshold * 0.99\n145:            description = \"Капитализация {} достигла ${:,.0f}, порог выше или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n146:        elif kind == \"cap_below\":\n147:            fired = cap <= threshold"},{"line":149,"snippet":"147:            fired = cap <= threshold\n148:            reset = cap > threshold * 1.01\n149:            description = \"Капитализация {} снизилась до ${:,.0f}, порог ниже или равно ${:,.0f}.\".format(coin[\"symbol\"],cap,threshold)\n150:        elif kind in {\"pump\",\"dump\"}:\n151:            old = reference_price(c,a[\"coin_id\"],now-int(a[\"window_seconds\"]))"},{"line":157,"snippet":"155:                    fired = change >= threshold\n156:                    reset = change < threshold * 0.5\n157:                    description = \"Резкий памп {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n158:                else:\n159:                    fired = change <= -threshold"},{"line":161,"snippet":"159:                    fired = change <= -threshold\n160:                    reset = change > -threshold * 0.5\n161:                    description = \"Резкое падение {}: {:+.2f}% за {} минут. Цена была ${:,.8f}, сейчас ${:,.8f}.\".format(coin[\"symbol\"],change,int(a[\"window_seconds\"])//60,old,price)\n162:        if reset and latched:\n163:            c.execute(\"UPDATE alerts SET latched=0 WHERE id=?\",(a[\"id\"],))"}],"total":[{"line":72,"snippet":"70:        if cid not in COINS:\n71:            continue\n72:        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\")}\n73:    if len(result[\"coins\"]) != len(COINS):\n74:        raise RuntimeError(\"incomplete market response\")"}]},"line_count":203,"mode":"0o640","path":"worker.py","redactions":0,"sanitized_sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","size":10300,"structure":{"defs":[{"end_line":23,"kind":"FunctionDef","line":14,"name":"connect"},{"end_line":28,"kind":"FunctionDef","line":25,"name":"atomic_json"},{"end_line":39,"kind":"FunctionDef","line":30,"name":"config"},{"end_line":75,"kind":"FunctionDef","line":41,"name":"fetch_market"},{"end_line":79,"kind":"FunctionDef","line":77,"name":"reference_price"},{"end_line":105,"kind":"FunctionDef","line":81,"name":"send_email"},{"end_line":117,"kind":"FunctionDef","line":107,"name":"portfolio_text"},{"end_line":175,"kind":"FunctionDef","line":119,"name":"evaluate"},{"end_line":190,"kind":"FunctionDef","line":177,"name":"cycle"},{"end_line":200,"kind":"FunctionDef","line":192,"name":"main"}],"parse_ok":true},"uid":0},{"exists":true,"gid":0,"gzip_sha256":"ea0cc3667dce3284eb67783fdec57d3077fef09e400a37f0d11ed4b59aad6479","hits":{"alert":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":8,"snippet":"6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":12,"snippet":"10:(()=>{\n11:  const install=()=>{\n12:    const button=document.getElementById(\"addAlert\");\n13:    const coinEl=document.getElementById(\"aCoin\");\n14:    const kindEl=document.getElementById(\"aKind\");"},{"line":20,"snippet":"18:    if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;}\n19:    button.type=\"button\";\n20:    let statusEl=document.getElementById(\"alertCreateStatusV3\");\n21:    if(!statusEl){\n22:      statusEl=document.createElement(\"div\");"},{"line":23,"snippet":"21:    if(!statusEl){\n22:      statusEl=document.createElement(\"div\");\n23:      statusEl.id=\"alertCreateStatusV3\";\n24:      statusEl.className=\"muted full\";\n25:      statusEl.setAttribute(\"aria-live\",\"polite\");"},{"line":65,"snippet":"63:        if(!beforeResponse.ok){throw new Error(\"предварительная проверка HTTP \"+beforeResponse.status);}\n64:        const before=await beforeResponse.json();\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\n66:        const response=await fetch(\"/api/alert\",{\n67:          method:\"POST\","},{"line":66,"snippet":"64:        const before=await beforeResponse.json();\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\n66:        const response=await fetch(\"/api/alert\",{\n67:          method:\"POST\",\n68:          credentials:\"same-origin\","},{"line":81,"snippet":"79:        const fresh=await verifyResponse.json();\n80:        const returnedId=Number(body.id||0);\n81:        const created=(fresh.alerts||[]).find(a=>{\n82:          const id=Number(a.id);\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);"},{"line":89,"snippet":"87:        try{state=fresh;}catch(_){}\n88:        try{render();}catch(error){console.error(error);}\n89:        try{renderAlerts();}catch(_){}\n90:        thresholdEl.value=\"\";\n91:        statusEl.className=\"good full\";"},{"line":109,"snippet":"107:})();\n108:</script>\n109:<script id=\"ALERT_LIST_RENDERER_V5\">\n110:(()=>{\n111:  let lastState=null;"},{"line":124,"snippet":"122:  const exchangeRate=data=>Number(data&&data.market&&data.market.usd_rub||0);\n123:  const converted=(data,value)=>Number(value||0)*(activeCurrency()===\"RUB\"&&exchangeRate(data)>0?exchangeRate(data):1);\n124:  const alertMoney=(data,value)=>new Intl.NumberFormat(activeCurrency()===\"RUB\"?\"ru-RU\":\"en-US\",{style:\"currency\",currency:activeCurrency(),minimumFractionDigits:2,maximumFractionDigits:2}).format(converted(data,value));\n125:  const make=(tag,className,value)=>{\n126:    const element=document.createElement(tag);"},{"line":131,"snippet":"129:    return element;\n130:  };\n131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;"},{"line":132,"snippet":"130:  };\n131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";"},{"line":133,"snippet":"131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";\n135:    return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);"},{"line":134,"snippet":"132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";\n135:    return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);\n136:  };"},{"line":135,"snippet":"133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";\n135:    return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);\n136:  };\n137:  const draw=data=>{"},{"line":138,"snippet":"136:  };\n137:  const draw=data=>{\n138:    const box=document.getElementById(\"alertList\");\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\n140:    lastState=data;"},{"line":139,"snippet":"137:  const draw=data=>{\n138:    const box=document.getElementById(\"alertList\");\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\n140:    lastState=data;\n141:    box.replaceChildren();"},{"line":142,"snippet":"140:    lastState=data;\n141:    box.replaceChildren();\n142:    if(data.alerts.length===0){\n143:      box.append(make(\"span\",\"muted\",\"Оповещений пока нет\"));\n144:      return;"},{"line":146,"snippet":"144:      return;\n145:    }\n146:    data.alerts.forEach(alert=>{\n147:      const row=make(\"div\",\"alert\");\n148:      row.dataset.alertRowV4=\"1\";"},{"line":147,"snippet":"145:    }\n146:    data.alerts.forEach(alert=>{\n147:      const row=make(\"div\",\"alert\");\n148:      row.dataset.alertRowV4=\"1\";\n149:      const left=make(\"div\");"},{"line":148,"snippet":"146:    data.alerts.forEach(alert=>{\n147:      const row=make(\"div\",\"alert\");\n148:      row.dataset.alertRowV4=\"1\";\n149:      const left=make(\"div\");\n150:      left.append(make(\"b\",\"\",alertLabel(data,alert)));"},{"line":150,"snippet":"148:      row.dataset.alertRowV4=\"1\";\n149:      const left=make(\"div\");\n150:      left.append(make(\"b\",\"\",alertLabel(data,alert)));\n151:      left.append(make(\"div\",\"muted\",alert.email||\"\"));\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};"},{"line":151,"snippet":"149:      const left=make(\"div\");\n150:      left.append(make(\"b\",\"\",alertLabel(data,alert)));\n151:      left.append(make(\"div\",\"muted\",alert.email||\"\"));\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);"},{"line":152,"snippet":"150:      left.append(make(\"b\",\"\",alertLabel(data,alert)));\n151:      left.append(make(\"div\",\"muted\",alert.email||\"\"));\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);"},{"line":154,"snippet":"152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");\n156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");"},{"line":155,"snippet":"153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");\n156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");\n157:      button.type=\"button\";"},{"line":158,"snippet":"156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");\n157:      button.type=\"button\";\n158:      button.dataset.alertDeleteV4=String(alert.id);\n159:      button.onclick=async event=>{\n160:        event.preventDefault();"},{"line":163,"snippet":"161:        button.disabled=true;\n162:        try{\n163:          const response=await fetch(\"/api/alert/\"+encodeURIComponent(alert.id),{method:\"DELETE\",credentials:\"same-origin\",cache:\"no-store\"});\n164:          if(response.status===401){location=\"/login\";return;}\n165:          if(!response.ok)throw new Error(\"HTTP \"+response.status);"}],"alerts":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":8,"snippet":"6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":65,"snippet":"63:        if(!beforeResponse.ok){throw new Error(\"предварительная проверка HTTP \"+beforeResponse.status);}\n64:        const before=await beforeResponse.json();\n65:        const beforeIds=new Set((before.alerts||[]).map(a=>Number(a.id)));\n66:        const response=await fetch(\"/api/alert\",{\n67:          method:\"POST\","},{"line":81,"snippet":"79:        const fresh=await verifyResponse.json();\n80:        const returnedId=Number(body.id||0);\n81:        const created=(fresh.alerts||[]).find(a=>{\n82:          const id=Number(a.id);\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);"},{"line":89,"snippet":"87:        try{state=fresh;}catch(_){}\n88:        try{render();}catch(error){console.error(error);}\n89:        try{renderAlerts();}catch(_){}\n90:        thresholdEl.value=\"\";\n91:        statusEl.className=\"good full\";"},{"line":139,"snippet":"137:  const draw=data=>{\n138:    const box=document.getElementById(\"alertList\");\n139:    if(!box||!data||!Array.isArray(data.alerts))return;\n140:    lastState=data;\n141:    box.replaceChildren();"},{"line":142,"snippet":"140:    lastState=data;\n141:    box.replaceChildren();\n142:    if(data.alerts.length===0){\n143:      box.append(make(\"span\",\"muted\",\"Оповещений пока нет\"));\n144:      return;"},{"line":146,"snippet":"144:      return;\n145:    }\n146:    data.alerts.forEach(alert=>{\n147:      const row=make(\"div\",\"alert\");\n148:      row.dataset.alertRowV4=\"1\";"},{"line":196,"snippet":"194:    if(box){\n195:      const observer=new MutationObserver(()=>{\n196:        if(lastState&&lastState.alerts&&lastState.alerts.length&&!box.querySelector(\"[data-alert-row-v4]\")){\n197:          queueMicrotask(()=>draw(lastState));\n198:        }"}],"chart":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":217,"snippet":"215:})();\n216:</script>\n217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}"},{"line":218,"snippet":"216:</script>\n217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}"},{"line":219,"snippet":"217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\n221:.tv-accordion-item:last-child{border-bottom:0}"},{"line":228,"snippet":"226:.tv-accordion-body{display:block;border-top:1px solid var(--l)}\n227:.tv-widget-slot{display:block;width:100%;height:430px;min-width:0;background:#081823}\n228:.tv-chart-message{min-height:20px;padding:8px 14px 12px}\n229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\n230:</style>"},{"line":231,"snippet":"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\n230:</style>\n231:<script src=\"/vendor/lightweight-charts.js\"></script>\n232:<script id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\">(()=>{\n233:  \"use strict\";"},{"line":232,"snippet":"230:</style>\n231:<script src=\"/vendor/lightweight-charts.js\"></script>\n232:<script id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\">(()=>{\n233:  \"use strict\";\n234:  const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";"},{"line":236,"snippet":"234:  const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";\n235:  const symbols=[\"HBAR\",\"POL\",\"AVAX\",\"LINK\"];\n236:  const charts=new Map();\n237:  const requests=new Map();\n238:  const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));"},{"line":297,"snippet":"295:  const waitLibrary=async()=>{\n296:    for(let attempt=0;attempt<150;attempt++){\n297:      if(window.LightweightCharts&&typeof window.LightweightCharts.createChart===\"function\")return window.LightweightCharts;\n298:      await sleep(100);\n299:    }"},{"line":300,"snippet":"298:      await sleep(100);\n299:    }\n300:    throw new Error(\"Библиотека Lightweight Charts не загрузилась\");\n301:  };\n302:  const widthOf=box=>Math.max(320,Math.floor(box.getBoundingClientRect().width||box.clientWidth||900));"},{"line":306,"snippet":"304:    if(typeof api.addCandlestickSeries===\"function\")return api.addCandlestickSeries(options);\n305:    if(typeof api.addSeries===\"function\"&&library.CandlestickSeries)return api.addSeries(library.CandlestickSeries,options);\n306:    throw new Error(\"Версия Lightweight Charts не поддерживается\");\n307:  };\n308:  const hideLegacy=()=>{"},{"line":309,"snippet":"307:  };\n308:  const hideLegacy=()=>{\n309:    for(const id of [\"chart\",\"chartTitle\",\"marketStatus\"]){\n310:      const element=document.getElementById(id);\n311:      if(!element)continue;"},{"line":318,"snippet":"316:    }\n317:  };\n318:  const createChart=async item=>{\n319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){"},{"line":320,"snippet":"318:  const createChart=async item=>{\n319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);\n322:      await afterLayout();"},{"line":321,"snippet":"319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);\n322:      await afterLayout();\n323:      if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}"},{"line":326,"snippet":"324:      return instance;\n325:    }\n326:    if(item.dataset.chartState===\"loading\"&&item._chartPromise)return item._chartPromise;\n327:    const box=item.querySelector(\".tv-widget-slot\");\n328:    const message=item.querySelector(\".tv-chart-message\");"},{"line":328,"snippet":"326:    if(item.dataset.chartState===\"loading\"&&item._chartPromise)return item._chartPromise;\n327:    const box=item.querySelector(\".tv-widget-slot\");\n328:    const message=item.querySelector(\".tv-chart-message\");\n329:    item.dataset.chartState=\"loading\";\n330:    message.textContent=\"Загрузка графика…\";"},{"line":329,"snippet":"327:    const box=item.querySelector(\".tv-widget-slot\");\n328:    const message=item.querySelector(\".tv-chart-message\");\n329:    item.dataset.chartState=\"loading\";\n330:    message.textContent=\"Загрузка графика…\";\n331:    item._chartPromise=(async()=>{"},{"line":331,"snippet":"329:    item.dataset.chartState=\"loading\";\n330:    message.textContent=\"Загрузка графика…\";\n331:    item._chartPromise=(async()=>{\n332:      try{\n333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);"},{"line":335,"snippet":"333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\n334:        await afterLayout();\n335:        if(!item.open){item.dataset.chartState=\"idle\";message.textContent=\"Нажмите, чтобы открыть график\";return null;}\n336:        box.replaceChildren();\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};"},{"line":338,"snippet":"336:        box.replaceChildren();\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\n339:        const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});\n340:        series.setData(candles);"},{"line":346,"snippet":"344:        if(observer)observer.observe(box);else window.addEventListener(\"resize\",resize,{passive:true});\n345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;"},{"line":347,"snippet":"345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\n349:        return instance;"},{"line":351,"snippet":"349:        return instance;\n350:      }catch(error){\n351:        item.dataset.chartState=\"error\";\n352:        message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353:        console.error(\"Gram1 chart error\",symbol,error);"},{"line":353,"snippet":"351:        item.dataset.chartState=\"error\";\n352:        message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353:        console.error(\"Gram1 chart error\",symbol,error);\n354:        return null;\n355:      }finally{item._chartPromise=null;}"},{"line":355,"snippet":"353:        console.error(\"Gram1 chart error\",symbol,error);\n354:        return null;\n355:      }finally{item._chartPromise=null;}\n356:    })();\n357:    return item._chartPromise;"},{"line":357,"snippet":"355:      }finally{item._chartPromise=null;}\n356:    })();\n357:    return item._chartPromise;\n358:  };\n359:  const init=async()=>{"},{"line":361,"snippet":"359:  const init=async()=>{\n360:    hideLegacy();\n361:    const root=document.getElementById(\"coinCharts\");\n362:    if(!root||root.dataset.interactiveReady===\"1\")return;\n363:    root.dataset.interactiveReady=\"1\";"}],"charts":[{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":217,"snippet":"215:})();\n216:</script>\n217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}"},{"line":218,"snippet":"216:</script>\n217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}"},{"line":231,"snippet":"229:@media(max-width:750px){.tv-accordion-head{padding:12px 14px}.tv-widget-slot{height:360px}}\n230:</style>\n231:<script src=\"/vendor/lightweight-charts.js\"></script>\n232:<script id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\">(()=>{\n233:  \"use strict\";"},{"line":232,"snippet":"230:</style>\n231:<script src=\"/vendor/lightweight-charts.js\"></script>\n232:<script id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7\">(()=>{\n233:  \"use strict\";\n234:  const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";"},{"line":236,"snippet":"234:  const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";\n235:  const symbols=[\"HBAR\",\"POL\",\"AVAX\",\"LINK\"];\n236:  const charts=new Map();\n237:  const requests=new Map();\n238:  const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));"},{"line":297,"snippet":"295:  const waitLibrary=async()=>{\n296:    for(let attempt=0;attempt<150;attempt++){\n297:      if(window.LightweightCharts&&typeof window.LightweightCharts.createChart===\"function\")return window.LightweightCharts;\n298:      await sleep(100);\n299:    }"},{"line":300,"snippet":"298:      await sleep(100);\n299:    }\n300:    throw new Error(\"Библиотека Lightweight Charts не загрузилась\");\n301:  };\n302:  const widthOf=box=>Math.max(320,Math.floor(box.getBoundingClientRect().width||box.clientWidth||900));"},{"line":306,"snippet":"304:    if(typeof api.addCandlestickSeries===\"function\")return api.addCandlestickSeries(options);\n305:    if(typeof api.addSeries===\"function\"&&library.CandlestickSeries)return api.addSeries(library.CandlestickSeries,options);\n306:    throw new Error(\"Версия Lightweight Charts не поддерживается\");\n307:  };\n308:  const hideLegacy=()=>{"},{"line":320,"snippet":"318:  const createChart=async item=>{\n319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);\n322:      await afterLayout();"},{"line":321,"snippet":"319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);\n322:      await afterLayout();\n323:      if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}"},{"line":326,"snippet":"324:      return instance;\n325:    }\n326:    if(item.dataset.chartState===\"loading\"&&item._chartPromise)return item._chartPromise;\n327:    const box=item.querySelector(\".tv-widget-slot\");\n328:    const message=item.querySelector(\".tv-chart-message\");"},{"line":329,"snippet":"327:    const box=item.querySelector(\".tv-widget-slot\");\n328:    const message=item.querySelector(\".tv-chart-message\");\n329:    item.dataset.chartState=\"loading\";\n330:    message.textContent=\"Загрузка графика…\";\n331:    item._chartPromise=(async()=>{"},{"line":335,"snippet":"333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\n334:        await afterLayout();\n335:        if(!item.open){item.dataset.chartState=\"idle\";message.textContent=\"Нажмите, чтобы открыть график\";return null;}\n336:        box.replaceChildren();\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};"},{"line":346,"snippet":"344:        if(observer)observer.observe(box);else window.addEventListener(\"resize\",resize,{passive:true});\n345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;"},{"line":347,"snippet":"345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\n349:        return instance;"},{"line":351,"snippet":"349:        return instance;\n350:      }catch(error){\n351:        item.dataset.chartState=\"error\";\n352:        message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353:        console.error(\"Gram1 chart error\",symbol,error);"},{"line":361,"snippet":"359:  const init=async()=>{\n360:    hideLegacy();\n361:    const root=document.getElementById(\"coinCharts\");\n362:    if(!root||root.dataset.interactiveReady===\"1\")return;\n363:    root.dataset.interactiveReady=\"1\";"},{"line":375,"snippet":"373:    };\n374:    items.forEach((item,index)=>{\n375:      item.dataset.chartState=\"idle\";\n376:      item.open=index===0;\n377:      item.addEventListener(\"toggle\",async()=>{"},{"line":397,"snippet":"395:\n396:<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 .title h2{width:100%;text-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>\n397:<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>"}],"coin":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":8,"snippet":"6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":13,"snippet":"11:  const install=()=>{\n12:    const button=document.getElementById(\"addAlert\");\n13:    const coinEl=document.getElementById(\"aCoin\");\n14:    const kindEl=document.getElementById(\"aKind\");\n15:    const thresholdEl=document.getElementById(\"aThreshold\");"},{"line":18,"snippet":"16:    const windowEl=document.getElementById(\"aWindow\");\n17:    const emailEl=document.getElementById(\"aEmail\");\n18:    if(!button||!coinEl||!kindEl||!thresholdEl||!windowEl||!emailEl){return;}\n19:    button.type=\"button\";\n20:    let statusEl=document.getElementById(\"alertCreateStatusV3\");"},{"line":30,"snippet":"28:    const ensureOptions=()=>{\n29:      try{\n30:        if(typeof state!==\"undefined\"&&state&&state.coins){\n31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){"},{"line":31,"snippet":"29:      try{\n30:        if(typeof state!==\"undefined\"&&state&&state.coins){\n31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");"},{"line":32,"snippet":"30:        if(typeof state!==\"undefined\"&&state&&state.coins){\n31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");\n34:          }"},{"line":33,"snippet":"31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");\n34:          }\n35:        }"},{"line":46,"snippet":"44:      const threshold=Number(String(thresholdEl.value||\"\").trim().replace(\",\",\".\"));\n45:      const payload={\n46:        coin_id:String(coinEl.value||\"\"),\n47:        kind:String(kindEl.value||\"\"),\n48:        threshold:threshold,"},{"line":54,"snippet":"52:      statusEl.className=\"muted full\";\n53:      statusEl.textContent=\"\";\n54:      if(!payload.coin_id){statusEl.className=\"bad full\";statusEl.textContent=\"Не выбрана монета\";return;}\n55:      if(!Number.isFinite(payload.threshold)||payload.threshold<=0){statusEl.className=\"bad full\";statusEl.textContent=\"Укажи положительный порог\";return;}\n56:      if(!payload.email||!payload.email.includes(\"@\")){statusEl.className=\"bad full\";statusEl.textContent=\"Укажи корректный email\";return;}"},{"line":84,"snippet":"82:          const id=Number(a.id);\n83:          const idIsNew=returnedId?id===returnedId:!beforeIds.has(id);\n84:          return idIsNew&&a.coin_id===payload.coin_id&&a.kind===payload.kind&&Number(a.threshold)===payload.threshold&&String(a.email)===payload.email;\n85:        });\n86:        if(!created){throw new Error(\"сервер не подтвердил новую запись оповещения\");}"},{"line":132,"snippet":"130:  };\n131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";"},{"line":152,"snippet":"150:      left.append(make(\"b\",\"\",alertLabel(data,alert)));\n151:      left.append(make(\"div\",\"muted\",alert.email||\"\"));\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);"},{"line":218,"snippet":"216:</script>\n217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}"},{"line":219,"snippet":"217:<style id=\"GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7\">#chart,#chartTitle,#marketStatus{display:none!important;width:0!important;height:0!important;min-height:0!important;margin:0!important;padding:0!important;border:0!important;overflow:hidden!important}\n218:#coinCharts{display:block!important;width:100%;margin-top:12px;border:1px solid var(--l);border-radius:16px;overflow:hidden;background:#081823}\n219:.coin-chart-title{margin-bottom:12px}\n220:.tv-accordion-item{display:block;border-bottom:1px solid var(--l);background:#081823}\n221:.tv-accordion-item:last-child{border-bottom:0}"},{"line":361,"snippet":"359:  const init=async()=>{\n360:    hideLegacy();\n361:    const root=document.getElementById(\"coinCharts\");\n362:    if(!root||root.dataset.interactiveReady===\"1\")return;\n363:    root.dataset.interactiveReady=\"1\";"}],"coins":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":30,"snippet":"28:    const ensureOptions=()=>{\n29:      try{\n30:        if(typeof state!==\"undefined\"&&state&&state.coins){\n31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){"},{"line":31,"snippet":"29:      try{\n30:        if(typeof state!==\"undefined\"&&state&&state.coins){\n31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");"},{"line":33,"snippet":"31:          const ids=Object.keys(state.coins);\n32:          if(ids.length&&coinEl.options.length!==ids.length){\n33:            coinEl.innerHTML=ids.map(id=>'<option value=\"'+id+'\">'+state.coins[id]+'</option>').join(\"\");\n34:          }\n35:        }"},{"line":132,"snippet":"130:  };\n131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";"}],"invested":[{"line":5,"snippet":"3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-ch"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""}],"move":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":338,"snippet":"336:        box.replaceChildren();\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\n339:        const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});\n340:        series.setData(candles);"},{"line":339,"snippet":"337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\n339:        const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});\n340:        series.setData(candles);\n341:        api.timeScale().fitContent();"},{"line":396,"snippet":"394:</script>\n395:\n396:<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 .title h2{width:100%;text-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>\n397:<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>"},{"line":397,"snippet":"395:\n396:<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 .title h2{width:100%;text-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>\n397:<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>"}],"moves":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":396,"snippet":"394:</script>\n395:\n396:<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 .title h2{width:100%;text-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>\n397:<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>"}],"portfolio":[{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""}],"position":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"}],"price":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":8,"snippet":"6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":114,"snippet":"112:  let loading=false;\n113:  const kindNames={\n114:    price_above:\"цена ≥\",\n115:    price_below:\"цена ≤\",\n116:    cap_above:\"капитализация ≥\","},{"line":115,"snippet":"113:  const kindNames={\n114:    price_above:\"цена ≥\",\n115:    price_below:\"цена ≤\",\n116:    cap_above:\"капитализация ≥\",\n117:    cap_below:\"капитализация ≤\","},{"line":154,"snippet":"152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");\n156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");"},{"line":155,"snippet":"153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");\n156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");\n157:      button.type=\"button\";"},{"line":338,"snippet":"336:        box.replaceChildren();\n337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\n339:        const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});\n340:        series.setData(candles);"},{"line":339,"snippet":"337:        const background=library.ColorType&&library.ColorType.Solid?{type:library.ColorType.Solid,color:\"#081823\"}:{color:\"#081823\"};\n338:        const api=library.createChart(box,{width:widthOf(box),height:430,layout:{background,textColor:\"#8fa5b6\"},grid:{vertLines:{color:\"rgba(143,165,182,.08)\"},horzLines:{color:\"rgba(143,165,182,.08)\"}},rightPriceScale:{borderColor:\"rgba(143,165,182,.20)\"},timeScale:{borderColor:\"rgba(143,165,182,.20)\",timeVisible:false,secondsVisible:false},handleScroll:{mouseWheel:true,pressedMouseMove:true,horzTouchDrag:true,vertTouchDrag:true},handleScale:{axisPressedMouseMove:true,mouseWheel:true,pinch:true}});\n339:        const series=makeSeries(api,library,{upColor:\"#63e6be\",downColor:\"#ff6b81\",wickUpColor:\"#63e6be\",wickDownColor:\"#ff6b81\",borderVisible:false,priceFormat:{type:\"price\",precision:8,minMove:0.00000001}});\n340:        series.setData(candles);\n341:        api.timeScale().fitContent();"}],"quantity":[{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":153,"snippet":"151:      left.append(make(\"div\",\"muted\",alert.email||\"\"));\n152:      const holding=(data.holdings&&data.holdings[alert.coin_id])||{};\n153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");"},{"line":155,"snippet":"153:      const quantity=Number(holding.quantity||0);\n154:      const isPrice=[\"price_above\",\"price_below\"].includes(alert.kind);\n155:      const sale=make(\"div\",\"sale\",isPrice?alertMoney(data,quantity*Number(alert.threshold||0)):\"—\");\n156:      const button=make(\"button\",\"btn ghost del\",\"Удалить\");\n157:      button.type=\"button\";"}],"symbol":[{"line":3,"snippet":"1:<!doctype html>\n2:<html lang=\"ru\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Gram Crypto</title>\n3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>"},{"line":6,"snippet":"4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-chart-sink\" aria-hidden=\"true\"></h2><span id=\"marketStatus\" class=\"status legacy-chart-sink\" aria-hidden=\"true\"></span><svg id=\"chart\" class=\"chart legacy-chart-sink\" viewBox=\"0 0 900 270\" preserveAspectRatio=\"none\" aria-hidden=\"true\" focusable=\"false\"></svg><div id=\"coinCharts\" data-chart-ui=\"GRAM1_INTERACTIVE_ACCORDION_V7\"><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"HBAR\" open><summary class=\"tv-accordion-head\"><span>HBAR / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"POL\"><summary class=\"tv-accordion-head\"><span>POL / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"AVAX\"><summary class=\"tv-accordion-head\"><span>AVAX / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details><details name=\"coin-chart\" class=\"tv-accordion-item\" data-symbol=\"LINK\"><summary class=\"tv-accordion-head\"><span>LINK / USDT</span><span class=\"tv-accordion-chevron\" aria-hidden=\"true\">⌄</span></summary><div class=\"tv-accordion-body\"><div class=\"tv-widget-slot\"></div><div class=\"tv-chart-message muted\">Нажмите, чтобы открыть график</div></div></details></div></div><div class=\"panel sharp-moves-panel\"><div class=\"title\"><h2>Резкие движения</h2></div><div id=\"moves\" class=\"moves\"></div></div></section>\n7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""},{"line":132,"snippet":"130:  };\n131:  const alertLabel=(data,alert)=>{\n132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";"},{"line":134,"snippet":"132:    const symbol=(data.coins&&data.coins[alert.coin_id])||alert.coin_id;\n133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";\n135:    return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);\n136:  };"},{"line":135,"snippet":"133:    const kind=kindNames[alert.kind]||alert.kind;\n134:    if([\"pump\",\"dump\"].includes(alert.kind))return symbol+\": \"+kind+\" \"+Number(alert.threshold).toLocaleString(\"ru-RU\",{maximumFractionDigits:12})+\"%\";\n135:    return symbol+\": \"+kind+\" \"+alertMoney(data,alert.threshold);\n136:  };\n137:  const draw=data=>{"},{"line":235,"snippet":"233:  \"use strict\";\n234:  const VERSION=\"GRAM1_INTERACTIVE_ACCORDION_V7\";\n235:  const symbols=[\"HBAR\",\"POL\",\"AVAX\",\"LINK\"];\n236:  const charts=new Map();\n237:  const requests=new Map();"},{"line":280,"snippet":"278:    return dedup;\n279:  };\n280:  const loadCandles=symbol=>{\n281:    if(requests.has(symbol))return requests.get(symbol);\n282:    const task=(async()=>{"},{"line":281,"snippet":"279:  };\n280:  const loadCandles=symbol=>{\n281:    if(requests.has(symbol))return requests.get(symbol);\n282:    const task=(async()=>{\n283:      const response=await fetch(\"/api/klines/\"+encodeURIComponent(symbol)+\"?limit=1000&interval=1M\",{cache:\"no-store\",credentials:\"same-origin\",headers:{Accept:\"application/json\"}});"},{"line":283,"snippet":"281:    if(requests.has(symbol))return requests.get(symbol);\n282:    const task=(async()=>{\n283:      const response=await fetch(\"/api/klines/\"+encodeURIComponent(symbol)+\"?limit=1000&interval=1M\",{cache:\"no-store\",credentials:\"same-origin\",headers:{Accept:\"application/json\"}});\n284:      if(response.status===401||response.status===403){location.href=\"/login\";throw new Error(\"Требуется повторный вход\");}\n285:      if(!response.ok)throw new Error(\"HTTP \"+response.status);"},{"line":291,"snippet":"289:      return normalizeCandles(raw);\n290:    })();\n291:    requests.set(symbol,task);\n292:    task.catch(()=>requests.delete(symbol));\n293:    return task;"},{"line":292,"snippet":"290:    })();\n291:    requests.set(symbol,task);\n292:    task.catch(()=>requests.delete(symbol));\n293:    return task;\n294:  };"},{"line":319,"snippet":"317:  };\n318:  const createChart=async item=>{\n319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);"},{"line":321,"snippet":"319:    const symbol=item.dataset.symbol;\n320:    if(item.dataset.chartState===\"ready\"){\n321:      const instance=charts.get(symbol);\n322:      await afterLayout();\n323:      if(instance){instance.api.applyOptions({width:widthOf(instance.box)});instance.api.timeScale().fitContent();}"},{"line":333,"snippet":"331:    item._chartPromise=(async()=>{\n332:      try{\n333:        const [candles,library]=await Promise.all([loadCandles(symbol),waitLibrary()]);\n334:        await afterLayout();\n335:        if(!item.open){item.dataset.chartState=\"idle\";message.textContent=\"Нажмите, чтобы открыть график\";return null;}"},{"line":345,"snippet":"343:        const observer=typeof ResizeObserver===\"function\"?new ResizeObserver(resize):null;\n344:        if(observer)observer.observe(box);else window.addEventListener(\"resize\",resize,{passive:true});\n345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";"},{"line":346,"snippet":"344:        if(observer)observer.observe(box);else window.addEventListener(\"resize\",resize,{passive:true});\n345:        const instance={symbol,api,series,box,observer,resize,candles};\n346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;"},{"line":348,"snippet":"346:        charts.set(symbol,instance);\n347:        item.dataset.chartState=\"ready\";\n348:        message.textContent=`${symbol} / USDT · 1M · ${candles.length} свечей · колесо — масштаб, перетаскивание — прокрутка`;\n349:        return instance;\n350:      }catch(error){"},{"line":352,"snippet":"350:      }catch(error){\n351:        item.dataset.chartState=\"error\";\n352:        message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353:        console.error(\"Gram1 chart error\",symbol,error);\n354:        return null;"},{"line":353,"snippet":"351:        item.dataset.chartState=\"error\";\n352:        message.textContent=\"Ошибка графика \"+symbol+\": \"+String(error&&error.message||error);\n353:        console.error(\"Gram1 chart error\",symbol,error);\n354:        return null;\n355:      }finally{item._chartPromise=null;}"},{"line":366,"snippet":"364:    root.dataset.chartUi=VERSION;\n365:    const items=[...root.querySelectorAll(\"details.tv-accordion-item\")];\n366:    if(items.length!==symbols.length)throw new Error(\"Некорректное количество секций графиков\");\n367:    let syncing=false;\n368:    const openOnly=item=>{"}],"total":[{"line":5,"snippet":"3:<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{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-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{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(--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-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>\n4:<body><main class=\"app\"><div class=\"top\"><div><div class=\"muted\">https://gram1.ru/</div><h1>Мой криптопортфель</h1></div><div class=\"top-actions\"><div class=\"currency-switch\"><button type=\"button\" class=\"currency-btn\" data-currency=\"USD\">USD</button><button type=\"button\" class=\"currency-btn\" data-currency=\"RUB\">RUB</button></div><form method=\"post\" action=\"/logout\"><button class=\"btn ghost\">Выйти</button></form></div></div>\n5:<section class=\"grid4\"><div class=\"card\"><span class=\"muted\">Стоимость портфеля</span><strong id=\"total\">—</strong><span id=\"day\" 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>\n6:<section id=\"coins\" class=\"coins\"></section><section class=\"layout\"><div class=\"panel\"><div class=\"title coin-chart-title\"><div><h2>Графики монет</h2><span class=\"muted\">Свечи Binance · 1 месяц · весь доступный период</span></div></div><h2 id=\"chartTitle\" class=\"legacy-ch"},{"line":9,"snippet":"7:<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></table></div></section>\n8:<section class=\"alerts\"><div class=\"panel\"><div class=\"title\"><h2>Новое оповещение</h2></div><div class=\"form\"><select id=\"aCoin\"></select><select id=\"aKind\"><option value=\"price_above\">Цена выше</option><option value=\"price_below\">Цена ниже</option><option value=\"cap_above\">Капитализация выше</option><option value=\"cap_below\">Капитализация ниже</option><option value=\"pump\">Резкий памп</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>\n9:<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\",currency:currency,minimumFractionDigits:d,maximumFractionDigits:d}).format(cv(n));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\\\"><span class=\\\"coin-symbol\\\">\"+state.coins[id]+\"</span><b class=\\\"coin-price\\\">\"+money(c.price,2)+\"</b></div><div class=\\\"\"+(ch>=0?\"good\":\"bad\")+\"\\\">\"+(ch>=0?\"+\":\"\")+ch.toFixed(2)+\"%</div><small class=\\\"muted\\\">Cap \"+compactMoney(c.market_cap)+\"</small></div>\"}).join(\"\");document.querySelectorAll(\".coin\").forEach(x=>{let 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(\"\");document.querySelectorAll(\"#rows tr\").forEach((tr,i)=>{let id=ids[i];let td=tr.cells[0];td.classList.add(\"asset-cell\");let img=document.createElement(\"img\");img.className=\"coin-icon table-coin-icon\";img.src=\"/coin-icon/\"+id;img.alt=state.coins[id];img.loading=\"lazy\";td.prepend(img);let symbol=td.querySelector(\"b\");if(symbol)symbol.classList.add(\"asset-symbol\")});totalEl=total;document.getElementById(\"total\").textContent=money(total,0);invested.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ч\""}]},"line_count":397,"mode":"0o644","path":"templates/index.html","redactions":0,"sanitized_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","size":38036,"structure":{"api":["/api/alert","/api/alert/","/api/holding","/api/klines/","/api/state"],"functions":[{"line":9,"name":"load"},{"line":9,"name":"render"},{"line":9,"name":"bindInputs"},{"line":9,"name":"draw"},{"line":9,"name":"renderMoves"},{"line":9,"name":"label"},{"line":9,"name":"renderAlerts"},{"line":9,"name":"fx"},{"line":9,"name":"money"},{"line":9,"name":"coin"},{"line":11,"name":"install"},{"line":28,"name":"ensureOptions"},{"line":121,"name":"activeCurrency"},{"line":123,"name":"converted"},{"line":124,"name":"alertMoney"},{"line":125,"name":"make"},{"line":131,"name":"alertLabel"},{"line":176,"name":"refresh"},{"line":192,"name":"install"},{"line":213,"name":"redrawForCurrency"},{"line":239,"name":"afterLayout"},{"line":282,"name":"task"},{"line":295,"name":"waitLibrary"},{"line":303,"name":"makeSeries"},{"line":308,"name":"hideLegacy"},{"line":342,"name":"resize"},{"line":359,"name":"init"},{"line":397,"name":"move"}],"ids":["ALERT_CREATE_OVERRIDE_V3","ALERT_LIST_RENDERER_V5","GRAM1_CHARTS_INTERACTIVE_ACCORDION_SCRIPT_V7","GRAM1_CHARTS_INTERACTIVE_ACCORDION_STYLE_V7","MOVE_CHARTS_TO_BOTTOM_ALLTIME_V1","SHARP_MOVES_LAYOUT_V2","aCoin","aEmail","aKind","aThreshold","aWindow","addAlert","alertCreateStatusV3","alertList","avgHeader","chart","chartTitle","coinCharts","coins","day","invested","marketStatus","moves","pnl","pnlp","rows","total","updated"]},"uid":0}],"sqlite":[{"path":"data/portfolio.db","quick_check":"ok","sha256":"a35a2f9567e76cacc420b8fddf335869dbb451c0e1d128467ffa06a59d1b36d7","size":1822720,"tables":[{"columns":[{"name":"id","notnull":0,"pk":1,"type":"INTEGER"},{"name":"coin_id","notnull":1,"pk":0,"type":"TEXT"},{"name":"kind","notnull":1,"pk":0,"type":"TEXT"},{"name":"threshold","notnull":1,"pk":0,"type":"REAL"},{"name":"window_seconds","notnull":1,"pk":0,"type":"INTEGER"},{"name":"email","notnull":1,"pk":0,"type":"TEXT"},{"name":"enabled","notnull":1,"pk":0,"type":"INTEGER"},{"name":"latched","notnull":1,"pk":0,"type":"INTEGER"},{"name":"last_triggered","notnull":0,"pk":0,"type":"INTEGER"}],"row_count":4,"symbols":{},"table":"alerts","usdc_rows":{}},{"columns":[{"name":"coin_id","notnull":0,"pk":1,"type":"TEXT"},{"name":"quantity","notnull":1,"pk":0,"type":"REAL"},{"name":"avg_buy","notnull":1,"pk":0,"type":"REAL"}],"row_count":4,"symbols":{},"table":"holdings","usdc_rows":{}},{"columns":[{"name":"ts","notnull":1,"pk":1,"type":"INTEGER"},{"name":"coin_id","notnull":1,"pk":2,"type":"TEXT"},{"name":"price","notnull":1,"pk":0,"type":"REAL"},{"name":"market_cap","notnull":1,"pk":0,"type":"REAL"},{"name":"volume","notnull":1,"pk":0,"type":"REAL"}],"row_count":15386,"symbols":{},"table":"market_history","usdc_rows":{}}]}]}
USDC_EXACT_SCOPE_REPORT_END
SOURCE_GZIP_B64_BEGIN {"chunks":3,"gzip_sha256":"e40905e690835200b2ebda03fc54e6c43aca829b08d26fe5982dc703be8c4d51","original_sha256":"3b55b78c015173eea66b67afb8fb9371f0d7d2e38a7679d4c433749f4c07301d","path":"app.py","redactions":1,"sanitized_sha256":"b6105195f16c4efb725a7629fd2d485d2c97cfab21fcb94344925d1a2dcf8bb2"}
SOURCE_GZIP_B64_CHUNK 0001/0003  
SOURCE_GZIP_B64_CHUNK 0002/0003  
SOURCE_GZIP_B64_CHUNK 0003/0003  
SOURCE_GZIP_B64_END app.py
SOURCE_GZIP_B64_BEGIN {"chunks":1,"gzip_sha256":"49befa11ea71cd7a4c7d0a41e10d092232ecf6a2438c3311495ea36b7dcc1cda","original_sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c","path":"worker.py","redactions":0,"sanitized_sha256":"4a7b16ece826fe2d938f392db6b0b5ca028dcb6ff02fdff4ab431f114edf0e5c"}
SOURCE_GZIP_B64_CHUNK 0001/0001  
SOURCE_GZIP_B64_END worker.py
SOURCE_GZIP_B64_BEGIN {"chunks":3,"gzip_sha256":"ea0cc3667dce3284eb67783fdec57d3077fef09e400a37f0d11ed4b59aad6479","original_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2","path":"templates/index.html","redactions":0,"sanitized_sha256":"3752846b9a2d47a2c82377d222e5f8603f78101eda11422bc8634567e26209d2"}
SOURCE_GZIP_B64_CHUNK 0001/0003  
SOURCE_GZIP_B64_CHUNK 0002/0003  
SOURCE_GZIP_B64_CHUNK 0003/0003  
SOURCE_GZIP_B64_END templates/index.html
{"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z","command_rc":0,"control_plane_error":null,"mutation_outcome":"NO_MUTATION","rc":0,"remote_rc":0,"remote_stderr_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","remote_stdout_sha256":"c7b4dca0bc02e3e303214ce7ff49f252a914a1fb0db74bb0496a9e140f28cec5","report_sha256":"0791be77ab77aad39a24810e3f850c07f83f3635941b683de983e477af1ce037","rollback_restored":false,"rollback_started":false,"source_count":3,"status":"OK","version":1}
HOMELAB_RESULT_CONTRACT={"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z","command_rc":1,"control_plane_error":null,"error":"0","error_type":"SystemExit","mutation_outcome":"NO_MUTATION","rc":1,"rollback_restored":false,"rollback_started":false,"status":"FAIL","version":1}
{"changes_made":false,"command_id":"GRAM1-SITE-USDC-V42-READ-ONLY-EXACT-SCOPE-R3-20260802T140000Z","command_rc":1,"control_plane_error":null,"error":"0","error_type":"SystemExit","mutation_outcome":"NO_MUTATION","rc":1,"rollback_restored":false,"rollback_started":false,"status":"FAIL","version":1}

OUTPUT_END
CHAT_OUTPUT_END
