diff --git a/README.md b/README.md
index 22ca5e1..aebb138 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,7 @@ Content-Type: application/json
{"updates":{"led":{"service_b":"96"},"api":{"port":"6001"}}}
```
-服务监控页面会列出本机 systemd 服务,并标识“已监控/未监控”。添加、编辑或删除监控使用 `PUT /api/v1/services`;启动或停止服务使用 `PUT /api/v1/services/control`,两者均要求 Bearer Token。SSH、向日葵、Tailscale 和 PIGWay 自身服务不能从 Web 停止,以免中断远程管理或控制进程。监控备注保存在配置文件的 `[service_notes]`,不会参与硬件 Agent 的监控判断。
+服务监控页面会列出本机 systemd 服务,并标识“已监控/未监控”。添加、编辑或删除监控使用 `PUT /api/v1/services`;启动或停止服务使用 `PUT /api/v1/services/control`,两者均要求 Bearer Token。API 服务不能通过自己的请求停止自身,其他服务由用户自行管理。监控备注保存在配置文件的 `[service_notes]`,不会参与硬件 Agent 的监控判断。
读取 token:
diff --git a/SHA256SUMS b/SHA256SUMS
index 1372f47..d433416 100644
--- a/SHA256SUMS
+++ b/SHA256SUMS
@@ -1,10 +1,10 @@
-5442d660875ec28909b1183e4e2aee604e9f2e82d229c3806498392e7a4546e3 README.md
+8e3d96c25cf6d6cd7d6dbbdf22829226ae29db5844a6c6d5867d81863538b3b0 README.md
72c46162c33f9587c6ccb01ad53fbaaf4ecec8cd21014b909e87e8707e253e36 app/oled_font_5x7.bin
d71d949643d31461f2e62ded3818e1a0f1711d1420756ad0cc32eb04cde0fad3 app/pigway_pi_control.py
-bf52a12d3208b5330f3e2980187e70330b930e950cfd5165b4e6605febfef65d app/pigway_pi_control_api.py
+7dc6c796febc012fa02e3ba9d66f151f585d8f77d4dabf9ca62c2b82423f92da app/pigway_pi_control_api.py
75fa662ed21138a8e394de83e46281dab039433cc0d7a8f787e1a049f4b47ec8 config/pigway-pi-control.conf
f34eac1df180a3da3a6998d650c06435fd89728c10b090f6162df82e53986dd0 install.sh
4f6e8db3698a62f57823d3298a0e6814593de6fa3d31e4751b7ea4a76e553e35 systemd/pigway-pi-control.service
aa21cef9a06593371ccff005335b6c0c826fbf4b5c48e4255b5f4b75ce6e9217 systemd/pigway-pi-control-api.service
9f59e7313ee58e687ae6dd29d175c67082674254ac5c5f54168b2735b64c20a4 uninstall.sh
-50ff3f73a16747233aa8400201f55b3b3eb579767e39af49d2fd0f6433718360 web/index.html
+08f3e276b44d615c18c394bf0508d7134f69d272e9f0e48ce5572cb5d4515dcd web/index.html
diff --git a/app/pigway_pi_control_api.py b/app/pigway_pi_control_api.py
index b890dad..b41f7ca 100644
--- a/app/pigway_pi_control_api.py
+++ b/app/pigway_pi_control_api.py
@@ -17,8 +17,7 @@ RGB_KEY_RE=re.compile(r"^(cpu|power|memory|storage|network|service|normal)_[rgb]
FLASH_KEY_RE=re.compile(r"^(warning|critical|emergency|normal)_flash_(on|off)_ms$")
NORMAL_MODES={"off","solid","flash","flow","breathe","marquee","rainbow","colorful"}
EFFECT_COLORS={"red","green","blue","yellow","purple","cyan","white"}
-PROTECTED_UNITS={"ssh.service","sshd.service","runawesun.service","tailscaled.service",
- "pigway-pi-control.service","pigway-pi-control-api.service"}
+PROTECTED_UNITS={"pigway-pi-control-api.service"}
def load_cfg():
cfg=configparser.ConfigParser(); cfg.read(CFG)
diff --git a/web/index.html b/web/index.html
index 1a6b057..a66352c 100644
--- a/web/index.html
+++ b/web/index.html
@@ -4,7 +4,7 @@
PIGWay Pi Control
@@ -33,14 +33,14 @@ function rgbGroup(values,prefix,title){const r=Number(values[`${prefix}_r`]||0),
function normalGroup(v){return`${field('led','normal_mode',v.normal_mode,'span2')}
${rgbGroup(v,'normal','自定义颜色')}
${field('led','normal_flash_on_ms',v.normal_flash_on_ms)}
${field('led','normal_flash_off_ms',v.normal_flash_off_ms)}
${field('led','normal_effect_speed',v.normal_effect_speed)}
${field('led','normal_effect_color',v.normal_effect_color)}
`}
function renderLed(v){const flash=['warning','critical','emergency'].map((x,i)=>`${['普通告警','严重告警','紧急告警'][i]}闪烁节奏 ${field('led',`${x}_flash_on_ms`,v[`${x}_flash_on_ms`])}${field('led',`${x}_flash_off_ms`,v[`${x}_flash_off_ms`])}
`).join(''),advanced=v.write_delay_ms===undefined?'':`硬件高级参数 ${field('led','write_delay_ms',v.write_delay_ms)}
`;return normalGroup(v)+`告警对象颜色 告警对象决定颜色,严重程度决定闪烁速度。
${alertColorGroups.map(([p,t])=>rgbGroup(v,p,t)).join('')}
`+flash+advanced}
function renderGeneral(s,v){const [t,d]=sectionMeta[s]||[s,''];return`${Object.entries(v).map(([k,x])=>field(s,k,x)).join('')||'
暂无配置项
'}
`}
-function renderServices(){return`已监控服务 可启动或停止非保护服务、编辑备注或删除监控。删除监控不会删除 systemd 服务。
`}
+function renderServices(){return`已监控服务 可启动、停止、编辑或删除监控;只有当前 API 服务不能在这里停止。
`}
function updateConditional(){const mode=document.querySelector('[data-section="led"][data-key="normal_mode"]')?.value||'off';document.querySelectorAll('.normal-field').forEach(x=>x.classList.toggle('hidden',!x.dataset.modes.split(' ').includes(mode)));updateSwatches()}function updateSwatches(){document.querySelectorAll('[data-swatch]').forEach(s=>{const p=s.dataset.swatch,read=c=>Math.max(0,Math.min(255,Number(document.querySelector(`[data-section="led"][data-key="${p}_${c}"]`)?.value||0))),hex='#'+['r','g','b'].map(c=>Math.round(read(c)).toString(16).padStart(2,'0')).join('');s.style.background=hex;const picker=document.querySelector(`[data-color-prefix="${p}"]`);if(picker)picker.value=hex})}
function renderConfig(){const ss=sectionOrder.filter(s=>original[s]);$('configMenu').innerHTML=ss.map((s,i)=>`${sectionMeta[s]?.[0]||s} `).join('');$('configRows').innerHTML=ss.map((s,i)=>`${sectionMeta[s]?.[0]||s} ${sectionMeta[s]?.[1]||''}
${s==='led'?renderLed(original[s]):s==='services'?renderServices():renderGeneral(s,original[s])} `).join('');$('configMenu').onclick=e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('[data-config-section]').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-config-page]').forEach(x=>x.classList.toggle('active',x.dataset.configPage===b.dataset.configSection));if(b.dataset.configSection==='services')loadServices()};$('configRows').oninput=e=>{if(e.target.matches('[data-color-prefix]')){const p=e.target.dataset.colorPrefix,v=e.target.value;['r','g','b'].forEach((c,i)=>{document.querySelector(`[data-section="led"][data-key="${p}_${c}"]`).value=parseInt(v.slice(1+i*2,3+i*2),16)})}if(e.target.matches('[data-section="led"],[data-color-prefix]'))updateConditional();if(e.target.matches('[data-section]'))markChanged()};$('refreshServices')?.addEventListener('click',loadServices);$('serviceSearch')?.addEventListener('input',renderServiceLists);$('saveMonitor')?.addEventListener('click',saveMonitor);$('resetMonitor')?.addEventListener('click',resetMonitorForm);updateConditional()}
function markChanged(){let n=0;document.querySelectorAll('#configRows [data-section]').forEach(i=>{if(i.value!==original[i.dataset.section][i.dataset.key])n++});$('saveState').textContent=n?`有 ${n} 项尚未保存`:'尚未修改';$('saveState').className=n?'':'muted'}
tabs.forEach(b=>b.onclick=()=>{tabs.forEach(x=>x.classList.toggle('active',x===b));['overview','logs','config'].forEach(id=>$(id).classList.toggle('hidden',id!==b.dataset.tab));if(b.dataset.tab==='logs')loadLogs();if(b.dataset.tab==='config')loadConfig()});async function get(url,opt){const r=await fetch(url,opt),text=await r.text();let j;try{j=JSON.parse(text)}catch{throw Error(`服务器返回异常 (${r.status})`)}if(!r.ok)throw Error(j.error||`请求失败 (${r.status})`);return j}function rows(target,items,empty='暂无数据'){$(target).innerHTML=items.length?items.join(''):`${empty}
`}
let serviceCache=[],editingServiceName='';function tokenHeaders(){const token=$('token').value.trim();localStorage.setItem('pigwayToken',token);return{'Content-Type':'application/json','Authorization':`Bearer ${token}`}}
async function loadServices(){if(!$('systemServices'))return;try{const d=await get('/api/v1/services');serviceCache=d.services;renderServiceLists()}catch(e){$('systemServices').innerHTML=`${esc(e.message)}
`}}
-function renderServiceLists(){if(!$('systemServices'))return;const q=$('serviceSearch').value.trim().toLowerCase(),filtered=serviceCache.filter(s=>!q||`${s.unit} ${s.description} ${s.note}`.toLowerCase().includes(q));rows('systemServices',filtered.map(s=>`${esc(s.unit)} ${esc(s.description||'暂无服务说明')} · ${esc(s.active==='active'?'正在运行':'未运行')} ${s.monitored?'已监控':'未监控'} ${s.monitored?'编辑监控':'加入监控'}
`),'没有匹配的服务');const monitored=serviceCache.filter(s=>s.monitored);rows('monitoredServices',monitored.map(s=>`${esc(s.monitor_name)} ${s.active==='active'?'运行中':'已停止'} ${esc(s.unit)}${s.note?` · ${esc(s.note)}`:''} ${s.active==='active'?'停止':'启动'} 编辑 删除监控
`),'尚未监控任何服务');document.querySelectorAll('[data-select-service]').forEach(b=>b.onclick=()=>selectService(b.dataset.selectService));document.querySelectorAll('[data-service-action]').forEach(b=>b.onclick=()=>serviceAction(b.dataset.unit,b.dataset.serviceAction));document.querySelectorAll('[data-delete-monitor]').forEach(b=>b.onclick=()=>deleteMonitor(b.dataset.deleteMonitor))}
+function renderServiceLists(){if(!$('systemServices'))return;const q=$('serviceSearch').value.trim().toLowerCase(),filtered=serviceCache.filter(s=>!q||`${s.unit} ${s.description} ${s.note}`.toLowerCase().includes(q));rows('systemServices',filtered.map(s=>`${esc(s.unit)} ${esc(s.description||'暂无服务说明')} · ${esc(s.active==='active'?'正在运行':'未运行')} ${s.monitored?'已监控':'未监控'} ${s.monitored?'编辑监控':'加入监控'}
`),'没有匹配的服务');const monitored=serviceCache.filter(s=>s.monitored);rows('monitoredServices',monitored.map(s=>`${esc(s.monitor_name)} ${s.active==='active'?'运行中':'已停止'} ${esc(s.unit)} 备注:${esc(s.note||'未填写')} ${s.active==='active'?'停止':'启动'} 编辑 删除监控
`),'尚未监控任何服务');document.querySelectorAll('[data-select-service]').forEach(b=>b.onclick=()=>selectService(b.dataset.selectService));document.querySelectorAll('[data-service-action]').forEach(b=>b.onclick=()=>serviceAction(b.dataset.unit,b.dataset.serviceAction));document.querySelectorAll('[data-delete-monitor]').forEach(b=>b.onclick=()=>deleteMonitor(b.dataset.deleteMonitor))}
function selectService(unit){const s=serviceCache.find(x=>x.unit===unit);if(!s)return;editingServiceName=s.monitor_name||'';$('monitorTarget').value=s.unit;$('monitorName').value=s.monitor_name||s.unit.replace(/\.service$/,'').replace(/[^A-Za-z0-9_.-]/g,'_').slice(0,16).toUpperCase();$('monitorNote').value=s.note||s.description||'';$('serviceFormTitle').textContent=s.monitored?'编辑服务监控':'新建服务监控';$('serviceMessage').textContent='已自动填写,可继续修改。';$('monitorName').scrollIntoView({behavior:'smooth',block:'center'})}
function resetMonitorForm(){editingServiceName='';$('monitorName').value='';$('monitorTarget').value='';$('monitorNote').value='';$('serviceFormTitle').textContent='新建服务监控';$('serviceMessage').textContent='';$('serviceMessage').className='muted'}
async function saveMonitor(){const body={operation:'save',name:$('monitorName').value.trim(),target:$('monitorTarget').value.trim(),note:$('monitorNote').value.trim(),previous_name:editingServiceName};try{$('serviceMessage').textContent='正在保存…';await get('/api/v1/services',{method:'PUT',headers:tokenHeaders(),body:JSON.stringify(body)});resetMonitorForm();await loadServices();$('serviceMessage').textContent='保存成功';$('serviceMessage').className='success'}catch(e){$('serviceMessage').textContent=e.message;$('serviceMessage').className='error'}}