alphax/static/config.html
2026-06-07 21:23:12 +08:00

66 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}AlphaX Agent — 配置中心{% endblock %}
{% block extra_head_css %}
<style>
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 44px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;margin-bottom:16px;flex-wrap:wrap}.page-head h1{font-size:28px;font-weight:950;letter-spacing:-.6px;color:var(--ink)}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.55;max-width:860px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.btn,.select{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:850;color:var(--ink)}.btn{cursor:pointer}.btn:disabled,.input:disabled,.textarea:disabled,.select:disabled{opacity:.55;cursor:not-allowed;background:var(--surface)}.layout{display:grid;grid-template-columns:380px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.config-list{display:grid}.config-row{padding:12px 14px;border-bottom:1px solid var(--hairline-soft);cursor:pointer;transition:.12s}.config-row:hover{background:var(--surface)}.config-row.active{background:rgba(66,98,255,.06);box-shadow:inset 3px 0 0 var(--blue)}.key{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:950;color:var(--ink);font-size:13px;margin-top:7px}.meta{margin-top:5px;color:var(--stone);font-size:11px;line-height:1.45}.badge{display:inline-flex;height:22px;align-items:center;border-radius:999px;border:1px solid var(--hairline-soft);background:var(--surface);padding:0 8px;font-size:11px;font-weight:900;color:var(--slate);margin:0 4px 4px 0}.badge.strategy{color:var(--blue);background:rgba(66,98,255,.07);border-color:rgba(66,98,255,.16)}.badge.system{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.badge.locked{color:var(--orange);background:rgba(255,174,0,.1);border-color:rgba(255,174,0,.22)}.badge.editable{color:var(--blue);background:rgba(66,98,255,.07);border-color:rgba(66,98,255,.16)}.editor{padding:14px}.field{margin-bottom:12px}.field label{display:block;font-size:11px;font-weight:900;color:var(--stone);margin-bottom:6px}.input,.textarea{width:100%;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:10px 12px;font-size:13px;color:var(--ink);outline:none}.textarea{min-height:420px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.55;resize:vertical}.msg{font-size:12px;min-height:18px;margin-top:8px}.msg.ok{color:var(--green)}.msg.err{color:var(--red)}.empty,.loading{padding:34px 16px;text-align:center;color:var(--stone);font-size:13px}.hint{padding:11px 12px;border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:14px}.guard{padding:10px 12px;border:1px solid rgba(255,174,0,.2);background:rgba(255,174,0,.08);border-radius:var(--radius-md);color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:12px}@media(max-width:980px){.layout{grid-template-columns:1fr}.shell{width:min(100% - 24px,1280px)}.textarea{min-height:320px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div>
<h1>配置中心</h1>
<p>运行期配置保存在 PostgreSQL但这里只展示适合在线调参的项目。密钥、数据库、SMTP、交易所底层连接和启动管理员只通过环境变量或专用页面管理。</p>
</div>
<div class="actions">
<select class="select" id="kindFilter" onchange="loadConfigs()">
<option value="all">全部</option>
<option value="strategy">策略运行时</option>
<option value="system">系统配置</option>
</select>
<button class="btn" onclick="newConfig('strategy')">新增策略</button>
<button class="btn" onclick="loadConfigs()">刷新</button>
</div>
</div>
<div class="hint">配置边界:这里适合调策略交易、通知开关、事件/舆情、监控和调度参数。API key、webhook 真实地址、SMTP 密码、DATABASE_URL、交易所 endpoint 等只放环境变量,避免线上配置漂移和误暴露。</div>
<div class="layout">
<section class="panel">
<div class="panel-head"><div class="panel-title">配置列表</div><div class="panel-note" id="countNote">--</div></div>
<div class="config-list" id="configList"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
<div class="panel-head"><div class="panel-title">编辑</div><div class="panel-note" id="editNote">选择一项配置</div></div>
<div class="editor">
<div class="field"><label>类型</label><select class="select" id="editKind"><option value="strategy">策略运行时</option><option value="system">系统配置</option></select></div>
<div class="field"><label>Key</label><input class="input" id="editKey" placeholder="例如 event_driven.sources 或 rules_override"></div>
<div class="field"><label>说明</label><input class="input" id="editDesc" placeholder="这项配置的用途"></div>
<div class="guard" id="guardNote">选择一项配置后会显示它的管理边界。</div>
<div class="field"><label>JSON 配置</label><textarea class="textarea" id="editJson" spellcheck="false">{}</textarea></div>
<div class="actions">
<button class="btn" id="saveBtn" onclick="saveCurrent()">保存</button>
<button class="btn" id="deleteBtn" onclick="deleteCurrent()">删除</button>
</div>
<div class="msg" id="msg"></div>
</div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var items=[], selected='';
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function fmtTime(t){if(!t)return'--';var d=new Date(t);if(isNaN(d.getTime()))return String(t).slice(0,16).replace('T',' ');return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')}
async function api(url,opt){var r=await fetch(url,opt||{});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
async function loadConfigs(){document.getElementById('configList').innerHTML='<div class="loading">加载中...</div>';try{var kind=document.getElementById('kindFilter').value;var d=await api('/api/runtime-config?kind='+encodeURIComponent(kind));items=d.items||[];document.getElementById('countNote').textContent=items.length+' 项';renderList();if(items.length&&!selected)selectItem(items[0].kind+'::'+items[0].config_key)}catch(e){document.getElementById('configList').innerHTML='<div class="empty">'+esc(e.message)+'</div>'}}
function currentItem(){return items.find(function(i){return i.kind+'::'+i.config_key===selected})||null}
function renderList(){if(!items.length){document.getElementById('configList').innerHTML='<div class="empty">暂无可在线调整配置</div>';return}document.getElementById('configList').innerHTML=items.map(function(x){var id=x.kind+'::'+x.config_key;var perm=x.editable===false?'只读':'可在线调整';var permClass=x.editable===false?'locked':'editable';return '<div class="config-row '+(id===selected?'active':'')+'" onclick="selectItem(\''+esc(id)+'\')"><div><span class="badge '+esc(x.kind)+'">'+(x.kind==='system'?'系统':'策略')+'</span><span class="badge '+permClass+'">'+perm+'</span></div><div class="key">'+esc(x.config_key)+'</div><div class="meta">'+esc(x.description||'--')+'<br>'+esc(x.category||'运行配置')+' · '+esc(x.source_of_truth||'配置中心')+' · 更新 '+fmtTime(x.updated_at)+'</div></div>'}).join('')}
function applyEditMode(x){var editable=!x||x.editable!==false;var deletable=!!(x&&x.delete_allowed);document.getElementById('editKind').disabled=!!x;document.getElementById('editKey').disabled=!!x;document.getElementById('editDesc').disabled=!editable;document.getElementById('editJson').disabled=!editable;document.getElementById('saveBtn').disabled=!editable;document.getElementById('deleteBtn').disabled=!deletable;var note='新建策略运行时配置。系统配置必须先在后端登记管理边界,不能从页面随意新增。';if(x){note=(x.editable===false?'只读配置。':'可在线调整。')+'来源:'+(x.source_of_truth||'配置中心')+'。'+(x.warning?' '+x.warning:'')}document.getElementById('guardNote').textContent=note}
function selectItem(id){selected=id;var x=currentItem();if(!x)return;document.getElementById('editKind').value=x.kind;document.getElementById('editKey').value=x.config_key;document.getElementById('editDesc').value=x.description||'';document.getElementById('editJson').value=JSON.stringify(x.config||{},null,2);document.getElementById('editNote').textContent=x.kind+' / '+x.config_key;document.getElementById('msg').textContent='';applyEditMode(x);renderList()}
function newConfig(kind){if(kind==='system'){document.getElementById('msg').className='msg err';document.getElementById('msg').textContent='系统配置不能从页面新增,请先在后端登记配置边界。';return}selected='';document.getElementById('editKind').value=kind;document.getElementById('editKey').value='';document.getElementById('editDesc').value='';document.getElementById('editJson').value='{}';document.getElementById('editNote').textContent='新增策略配置';document.getElementById('msg').textContent='';applyEditMode(null);renderList()}
async function saveCurrent(){var msg=document.getElementById('msg');try{var x=currentItem();if(x&&x.editable===false)throw new Error('该配置不可在配置中心编辑');var kind=document.getElementById('editKind').value;var key=document.getElementById('editKey').value.trim();if(!key)throw new Error('请填写 Key');var config=JSON.parse(document.getElementById('editJson').value||'{}');await api('/api/runtime-config/'+encodeURIComponent(kind)+'/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({config:config,description:document.getElementById('editDesc').value||''})});msg.className='msg ok';msg.textContent='已保存到 PostgreSQL';selected=kind+'::'+key;await loadConfigs()}catch(e){msg.className='msg err';msg.textContent=e.message}}
async function deleteCurrent(){var x=currentItem();if(x&&x.delete_allowed===false){document.getElementById('msg').className='msg err';document.getElementById('msg').textContent='该配置不允许从配置中心删除';return}var kind=document.getElementById('editKind').value;var key=document.getElementById('editKey').value.trim();if(!key)return;if(!confirm('确认删除 '+key+' ?'))return;try{await api('/api/runtime-config/'+encodeURIComponent(kind)+'/'+encodeURIComponent(key),{method:'DELETE'});selected='';newConfig('strategy');await loadConfigs()}catch(e){document.getElementById('msg').className='msg err';document.getElementById('msg').textContent=e.message}}
loadConfigs();
</script>
{% endblock %}