zhuce6.html 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width,initial-scale=1">
  6. <title>zhuce6 dashboard</title>
  7. <link rel="preconnect" href="https://fonts.googleapis.com">
  8. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
  9. <style>
  10. :root {
  11. --bg: #faf6f0;
  12. --bg2: #f3ece2;
  13. --card: #fffcf7;
  14. --card-hover: #f9f3ea;
  15. --border: #e8dcc8;
  16. --text: #3d3427;
  17. --muted: #9a8a72;
  18. --accent: #c07a38;
  19. --accent2: #a86528;
  20. --good: #3a8f5c;
  21. --good-bg: rgba(58,143,92,.1);
  22. --warn: #c08b2e;
  23. --warn-bg: rgba(192,139,46,.12);
  24. --bad: #c0503a;
  25. --bad-bg: rgba(192,80,58,.1);
  26. --radius: 14px;
  27. }
  28. * { box-sizing: border-box; margin: 0; padding: 0; }
  29. body {
  30. background: radial-gradient(ellipse at top right, rgba(198,146,74,.08), transparent 50%), linear-gradient(180deg, var(--bg) 0%, var(--bg2) 100%);
  31. color: var(--text);
  32. font: 13px/1.5 'Inter', -apple-system, sans-serif;
  33. min-height: 100vh;
  34. }
  35. /* Layout */
  36. .page { max-width: 1520px; margin: 0 auto; padding: 18px; }
  37. .app-shell { display: grid; grid-template-columns: 176px minmax(0, 1fr); gap: 14px; align-items: start; }
  38. .nav-shell {
  39. position: sticky; top: 20px;
  40. background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  41. padding: 12px 10px; box-shadow: 0 2px 12px rgba(120,80,30,.06);
  42. }
  43. .nav-title { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .08em; margin-bottom: 8px; padding: 0 6px; }
  44. .nav-list { display: grid; gap: 6px; }
  45. .nav-btn {
  46. width: 100%; text-align: left; border: 1px solid transparent; border-radius: 10px;
  47. background: transparent; color: var(--text); padding: 9px 10px; cursor: pointer;
  48. font: inherit; transition: .15s;
  49. }
  50. .nav-btn:hover { background: rgba(192,122,56,.06); border-color: rgba(192,122,56,.12); }
  51. .nav-btn.active { background: rgba(192,122,56,.12); border-color: rgba(192,122,56,.25); color: var(--accent2); }
  52. .nav-label { font-size: 11px; font-weight: 700; }
  53. .nav-sub { font-size: 9px; color: var(--muted); margin-top: 2px; line-height: 1.25; }
  54. .content-stack { display: grid; gap: 16px; min-width: 0; }
  55. .view-panel { display: none; gap: 16px; }
  56. .view-panel.active { display: grid; }
  57. .stack { display: grid; gap: 16px; }
  58. .two-col { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(280px, .9fr); gap: 16px; }
  59. /* Header */
  60. .header {
  61. display: flex; align-items: center; justify-content: space-between;
  62. padding: 16px 20px; margin-bottom: 20px;
  63. background: var(--card); border: 1px solid var(--border);
  64. border-radius: var(--radius);
  65. }
  66. .header h1 { font-size: 18px; font-weight: 700; letter-spacing: -.02em; }
  67. .header-right { display: flex; align-items: center; gap: 12px; }
  68. .pulse {
  69. width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  70. background: var(--good); box-shadow: 0 0 6px rgba(58,143,92,.4);
  71. animation: pulse 2s infinite;
  72. }
  73. .pulse.bad { background: var(--bad); box-shadow: 0 0 6px rgba(192,80,58,.4); }
  74. @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
  75. #status-text { font-size: 12px; font-weight: 600; color: var(--good); }
  76. #status-text.bad { color: var(--bad); }
  77. #updated { font-size: 11px; color: var(--muted); }
  78. .btn-manage {
  79. padding: 6px 14px; font-size: 12px; font-weight: 600;
  80. background: var(--accent); color: #fff; border: none; border-radius: 8px;
  81. text-decoration: none; transition: .15s;
  82. }
  83. .btn-manage:hover { background: var(--accent2); }
  84. .btn-inline {
  85. padding: 6px 12px; font-size: 11px; font-weight: 600;
  86. background: rgba(192,122,56,.1); color: var(--accent2); border: 1px solid rgba(192,122,56,.2);
  87. border-radius: 8px; cursor: pointer; transition: .15s;
  88. }
  89. .btn-inline:hover { background: rgba(192,122,56,.16); }
  90. .btn-inline:disabled { opacity: .6; cursor: default; }
  91. .hero-card { box-shadow: 0 2px 12px rgba(120,80,30,.06); }
  92. .section { box-shadow: 0 2px 12px rgba(120,80,30,.06); }
  93. /* Hero metrics */
  94. .hero { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; }
  95. .hero-card {
  96. background: var(--card); border: 1px solid var(--border);
  97. border-radius: var(--radius); padding: 16px 18px;
  98. position: relative; overflow: hidden;
  99. }
  100. .hero-card::before {
  101. content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  102. }
  103. .hero-card.c-green::before { background: linear-gradient(90deg, #3a8f5c, #5cb87a); }
  104. .hero-card.c-blue::before { background: linear-gradient(90deg, #c07a38, #d4944f); }
  105. .hero-card.c-purple::before { background: linear-gradient(90deg, #8b6f4e, #a68a64); }
  106. .hero-card.c-amber::before { background: linear-gradient(90deg, #c08b2e, #d4a24a); }
  107. .hero-label { font-size: 11px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
  108. .hero-value { font-size: 28px; font-weight: 700; margin-top: 4px; letter-spacing: -.03em; }
  109. .hero-sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
  110. /* Main grid */
  111. .main { display: grid; grid-template-columns: 1fr 340px; gap: 16px; }
  112. /* Section cards */
  113. .section {
  114. background: var(--card); border: 1px solid var(--border);
  115. border-radius: var(--radius); overflow: hidden;
  116. }
  117. .section-head {
  118. display: flex; align-items: center; justify-content: space-between;
  119. padding: 14px 18px; border-bottom: 1px solid var(--border);
  120. }
  121. .section-title { font-size: 13px; font-weight: 700; }
  122. .section-meta { font-size: 11px; color: var(--muted); }
  123. .section-body { padding: 14px 18px; }
  124. /* Failure analysis */
  125. .fail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  126. .fail-bar-wrap { margin-bottom: 8px; }
  127. .fail-bar-label { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; }
  128. .fail-bar-label .name { color: var(--text); font-weight: 500; }
  129. .fail-bar-label .count { color: var(--muted); }
  130. .fail-bar { height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; }
  131. .fail-bar-fill { height: 100%; border-radius: 3px; background: var(--bad); transition: width .3s; }
  132. /* Task cards */
  133. .tasks { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 10px; }
  134. .task-card {
  135. padding: 12px 14px; background: var(--bg2);
  136. border: 1px solid var(--border); border-radius: 10px; min-width: 0;
  137. }
  138. .task-name { font-size: 12px; font-weight: 700; text-transform: uppercase; }
  139. .task-status { display: inline-block; margin-top: 4px; padding: 2px 8px; font-size: 10px; font-weight: 700; border-radius: 6px; text-transform: uppercase; }
  140. .task-status.running { background: var(--good-bg); color: var(--good); }
  141. .task-status.idle { background: rgba(107,113,148,.15); color: var(--muted); }
  142. .task-status.error { background: var(--bad-bg); color: var(--bad); }
  143. .task-stats { margin-top: 8px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 8px; font-size: 11px; color: var(--muted); }
  144. .task-stats span { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  145. /* Activity stream table */
  146. .stream { max-height: 320px; overflow-y: auto; }
  147. .stream::-webkit-scrollbar { width: 4px; }
  148. .stream::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
  149. table { width: 100%; border-collapse: collapse; font-size: 12px; }
  150. th { padding: 8px 12px; text-align: left; font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--card); }
  151. td { padding: 7px 12px; border-bottom: 1px solid rgba(232,220,200,.5); vertical-align: middle; }
  152. tr:hover td { background: rgba(192,122,56,.04); }
  153. .mono { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 11px; }
  154. /* Badges */
  155. .badge { display: inline-block; padding: 2px 8px; font-size: 10px; font-weight: 700; border-radius: 6px; }
  156. .badge-ok { background: var(--good-bg); color: var(--good); }
  157. .badge-fail { background: var(--bad-bg); color: var(--bad); }
  158. .badge-warn { background: var(--warn-bg); color: var(--warn); }
  159. /* Sidebar */
  160. .sidebar { display: flex; flex-direction: column; gap: 16px; }
  161. .proxy-list { max-height: 400px; overflow-y: auto; }
  162. .proxy-list::-webkit-scrollbar { width: 4px; }
  163. .proxy-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
  164. .proxy-item {
  165. display: flex; align-items: center; gap: 10px;
  166. padding: 8px 0; border-bottom: 1px solid rgba(232,220,200,.4);
  167. }
  168. .proxy-item:last-child { border: none; }
  169. .proxy-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
  170. .proxy-dot.active { background: var(--good); }
  171. .proxy-dot.idle { background: var(--warn); }
  172. .proxy-dot.disabled { background: var(--bad); }
  173. .proxy-name { font-size: 12px; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  174. .proxy-bar { width: 60px; height: 4px; background: var(--bg); border-radius: 2px; overflow: hidden; flex-shrink: 0; }
  175. .proxy-bar-fill { height: 100%; background: var(--good); border-radius: 2px; }
  176. .proxy-ratio { font-size: 10px; color: var(--muted); width: 40px; text-align: right; flex-shrink: 0; }
  177. /* File list */
  178. .file-item { padding: 8px 0; border-bottom: 1px solid rgba(232,220,200,.4); }
  179. .file-item:last-child { border: none; }
  180. .file-name { font-size: 12px; font-weight: 500; word-break: break-all; }
  181. .file-meta { font-size: 10px; color: var(--muted); margin-top: 2px; }
  182. .mini-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 10px; margin-bottom: 12px; align-items: stretch; }
  183. .mini-grid > * { min-width: 0; }
  184. .mini-card {
  185. background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
  186. padding: 10px 12px; min-width: 0; overflow: hidden;
  187. }
  188. .mini-label {
  189. font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em;
  190. white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  191. }
  192. .mini-value {
  193. font-size: clamp(18px, 1.8vw, 20px); font-weight: 700; margin-top: 3px; letter-spacing: -.02em;
  194. line-height: 1.18; min-width: 0; max-width: 100%; overflow-wrap: anywhere; word-break: break-word;
  195. }
  196. .mini-sub {
  197. font-size: 10px; color: var(--muted); margin-top: 2px;
  198. min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  199. }
  200. .compact-list { display: grid; gap: 8px; }
  201. .compact-item {
  202. padding: 8px 10px; border: 1px solid rgba(232,220,200,.55); border-radius: 10px; background: rgba(243,236,226,.55);
  203. }
  204. .compact-item .line1 { font-size: 11px; font-weight: 600; color: var(--text); }
  205. .compact-item .line2 { font-size: 10px; color: var(--muted); margin-top: 2px; }
  206. .kv-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; }
  207. .kv-item { padding: 8px 0; border-bottom: 1px solid rgba(232,220,200,.4); }
  208. .kv-item:nth-last-child(-n+2) { border-bottom: none; }
  209. .kv-key { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
  210. .kv-val { font-size: 12px; color: var(--text); margin-top: 2px; word-break: break-all; }
  211. .compact-scroll { max-height: 320px; overflow-y: auto; padding-right: 4px; }
  212. .compact-scroll::-webkit-scrollbar { width: 4px; }
  213. .compact-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
  214. .domain-banner {
  215. display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 12px;
  216. padding: 12px 14px; background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
  217. min-width: 0; overflow: hidden;
  218. }
  219. .domain-name {
  220. font-size: 14px; font-weight: 700; color: var(--accent2);
  221. min-width: 0; max-width: 100%; line-height: 1.25; overflow-wrap: anywhere; word-break: break-word;
  222. }
  223. .domain-meta {
  224. font-size: 11px; color: var(--muted);
  225. min-width: 0; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  226. }
  227. /* Alert */
  228. .alert { display: none; padding: 12px 16px; margin-bottom: 16px; background: var(--bad-bg); border: 1px solid rgba(248,113,113,.2); border-radius: var(--radius); color: var(--bad); font-size: 12px; }
  229. /* Empty state */
  230. .empty { padding: 20px; text-align: center; color: var(--muted); font-size: 12px; }
  231. .settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
  232. .settings-card {
  233. background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  234. box-shadow: 0 2px 12px rgba(120,80,30,.06); overflow: hidden;
  235. }
  236. .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  237. .field { display: grid; gap: 6px; }
  238. .field.full { grid-column: 1 / -1; }
  239. .field label { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
  240. .input, .textarea {
  241. width: 100%; background: var(--bg2); color: var(--text); border: 1px solid var(--border);
  242. border-radius: 10px; padding: 10px 12px; font: inherit;
  243. }
  244. .textarea { min-height: 108px; resize: vertical; }
  245. .control-row, .settings-actions, .settings-status-row {
  246. display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  247. }
  248. .btn-danger {
  249. background: rgba(192,80,58,.12); color: var(--bad); border-color: rgba(192,80,58,.2);
  250. }
  251. .btn-danger:hover { background: rgba(192,80,58,.18); }
  252. .status-pill {
  253. display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
  254. border-radius: 999px; font-size: 11px; font-weight: 700;
  255. }
  256. .status-pill.ok { background: var(--good-bg); color: var(--good); }
  257. .status-pill.warn { background: var(--warn-bg); color: var(--warn); }
  258. .status-pill.error { background: var(--bad-bg); color: var(--bad); }
  259. .status-pill::before {
  260. content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor;
  261. }
  262. .settings-feedback {
  263. display: none; padding: 10px 12px; border-radius: 10px; font-size: 12px; border: 1px solid transparent;
  264. }
  265. .settings-feedback.ok { display: block; background: var(--good-bg); color: var(--good); border-color: rgba(58,143,92,.18); }
  266. .settings-feedback.error { display: block; background: var(--bad-bg); color: var(--bad); border-color: rgba(192,80,58,.18); }
  267. .dep-grid { display: grid; gap: 10px; }
  268. .dep-item {
  269. display: flex; justify-content: space-between; gap: 12px; align-items: center;
  270. padding: 10px 12px; background: rgba(243,236,226,.55); border: 1px solid rgba(232,220,200,.55); border-radius: 10px;
  271. }
  272. .dep-copy { min-width: 0; }
  273. .dep-copy strong { display: block; font-size: 12px; }
  274. .dep-copy span { display: block; font-size: 10px; color: var(--muted); margin-top: 2px; word-break: break-word; }
  275. /* Responsive */
  276. @media (max-width: 1100px) {
  277. .app-shell { grid-template-columns: 1fr; }
  278. .nav-shell { position: static; }
  279. .hero { grid-template-columns: repeat(3, 1fr); }
  280. .main { grid-template-columns: 1fr; }
  281. .tasks { grid-template-columns: repeat(3, 1fr); }
  282. .mini-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  283. .two-col { grid-template-columns: 1fr; }
  284. }
  285. @media (max-width: 640px) {
  286. .hero { grid-template-columns: 1fr; }
  287. .tasks { grid-template-columns: 1fr; }
  288. .fail-grid { grid-template-columns: 1fr; }
  289. .mini-grid { grid-template-columns: 1fr; }
  290. .kv-grid { grid-template-columns: 1fr; }
  291. .settings-grid { grid-template-columns: 1fr; }
  292. .form-grid { grid-template-columns: 1fr; }
  293. .page { padding: 12px; }
  294. .nav-btn { padding: 9px 10px; }
  295. }
  296. </style>
  297. </head>
  298. <body>
  299. <div class="page">
  300. <!-- Header -->
  301. <header class="header">
  302. <div>
  303. <h1>zhuce6 Monitor</h1>
  304. <div id="updated" style="margin-top:2px">加载中...</div>
  305. </div>
  306. <div class="header-right">
  307. <div id="pulse" class="pulse"></div>
  308. <span id="status-text">LOADING</span>
  309. <a id="btn-cpa-manage" class="btn-manage" data-cpa-only href="#" target="_blank" rel="noopener noreferrer">CPA 管理</a>
  310. </div>
  311. </header>
  312. <!-- Alert -->
  313. <div id="alert" class="alert"></div>
  314. <!-- Hero Metrics -->
  315. <div class="app-shell">
  316. <aside class="nav-shell">
  317. <div class="nav-title">Quick Views</div>
  318. <div class="nav-list" id="nav-list">
  319. <button class="nav-btn active" data-view="overview">
  320. <div class="nav-label">总览</div>
  321. <div class="nav-sub">状态, 任务, 总体 KPI</div>
  322. </button>
  323. <button class="nav-btn" data-view="active-domain">
  324. <div class="nav-label">当前活跃域</div>
  325. <div class="nav-sub">只看当前域故障与成功</div>
  326. </button>
  327. <button class="nav-btn" data-view="cumulative">
  328. <div class="nav-label">启动至今</div>
  329. <div class="nav-sub">累计分析与全量流水</div>
  330. </button>
  331. <button class="nav-btn" data-view="rotate" data-cpa-only>
  332. <div class="nav-label">Rotate / CPA 401</div>
  333. <div class="nav-sub">主池轮换与清理</div>
  334. </button>
  335. <button class="nav-btn" data-view="proxy">
  336. <div class="nav-label">代理池</div>
  337. <div class="nav-sub">节点状态与成功率</div>
  338. </button>
  339. <button class="nav-btn" data-view="storage">
  340. <div class="nav-label">写盘 / 成功</div>
  341. <div class="nav-sub">写盘记录与成功账号</div>
  342. </button>
  343. <button class="nav-btn" data-view="survival">
  344. <div class="nav-label">存活实验</div>
  345. <div class="nav-sub">固定 cohort 401 存活时长</div>
  346. </button>
  347. </div>
  348. </aside>
  349. <div class="content-stack">
  350. <section id="overview-view" class="view-panel active">
  351. <div id="hero" class="hero"></div>
  352. <div class="two-col">
  353. <div class="stack">
  354. <section class="section">
  355. <div class="section-head">
  356. <div class="section-title">⚡ 任务状态</div>
  357. <div class="section-meta" id="task-meta">-</div>
  358. </div>
  359. <div class="section-body">
  360. <div id="tasks" class="tasks"></div>
  361. </div>
  362. </section>
  363. <section class="section">
  364. <div class="section-head">
  365. <div class="section-title">📊 启动至今故障分析</div>
  366. <div class="section-meta" id="overview-fail-meta">-</div>
  367. </div>
  368. <div class="section-body">
  369. <div class="fail-grid" id="overview-fail-grid"></div>
  370. </div>
  371. </section>
  372. </div>
  373. <div class="stack">
  374. <section class="section">
  375. <div class="section-head">
  376. <div class="section-title">🧭 当前活跃域</div>
  377. <div class="section-meta" id="active-domain-meta">-</div>
  378. </div>
  379. <div class="section-body">
  380. <div class="domain-banner">
  381. <div id="active-domain-name" class="domain-name">-</div>
  382. <div id="active-domain-sub" class="domain-meta">-</div>
  383. </div>
  384. <div id="active-domain-kpis" class="mini-grid" style="margin-top:12px"></div>
  385. </div>
  386. </section>
  387. <section class="section">
  388. <div class="section-head">
  389. <div class="section-title">⚙️ 当前运行配置</div>
  390. <div class="section-meta" id="cfg-meta">-</div>
  391. </div>
  392. <div class="section-body">
  393. <div id="cfg-wrap" class="kv-grid"></div>
  394. </div>
  395. </section>
  396. <section class="section">
  397. <div class="section-head">
  398. <div class="section-title">🗓️ 批次补号策略</div>
  399. <div class="section-meta" id="plan-meta">-</div>
  400. </div>
  401. <div class="section-body">
  402. <div id="plan-wrap" class="mini-grid"></div>
  403. </div>
  404. </section>
  405. </div>
  406. </div>
  407. </section>
  408. <section id="active-domain-view" class="view-panel">
  409. <div class="stack">
  410. <section class="section">
  411. <div class="section-head">
  412. <div class="section-title">🧭 当前活跃域故障分析</div>
  413. <div class="section-meta" id="fail-meta">-</div>
  414. </div>
  415. <div class="section-body">
  416. <div class="domain-banner">
  417. <div id="active-fail-domain-name" class="domain-name">-</div>
  418. <div id="active-fail-domain-sub" class="domain-meta">-</div>
  419. </div>
  420. <div class="fail-grid" id="fail-grid" style="margin-top:12px"></div>
  421. </div>
  422. </section>
  423. <section class="section">
  424. <div class="section-head">
  425. <div class="section-title">📋 当前活跃域最近记录</div>
  426. <div class="section-meta" id="stream-meta">-</div>
  427. </div>
  428. <div class="stream" id="stream-wrap"></div>
  429. </section>
  430. <section class="section">
  431. <div class="section-head">
  432. <div class="section-title">✅ 当前活跃域成功记录</div>
  433. <div class="section-meta" id="success-meta">-</div>
  434. </div>
  435. <div class="section-body compact-scroll" id="success-wrap"></div>
  436. </section>
  437. </div>
  438. </section>
  439. <section id="cumulative-view" class="view-panel">
  440. <div class="stack">
  441. <section class="section">
  442. <div class="section-head">
  443. <div class="section-title">📈 启动至今累计故障分析</div>
  444. <div class="section-meta" id="cumulative-meta">-</div>
  445. </div>
  446. <div class="section-body">
  447. <div id="cumulative-grid" class="mini-grid"></div>
  448. <div class="fail-grid" id="cumulative-fail-grid" style="margin-top:12px"></div>
  449. </div>
  450. </section>
  451. <section class="section">
  452. <div class="section-head">
  453. <div class="section-title">📚 启动至今最近流水</div>
  454. <div class="section-meta" id="cumulative-stream-meta">-</div>
  455. </div>
  456. <div class="stream" id="cumulative-stream-wrap"></div>
  457. </section>
  458. </div>
  459. </section>
  460. <section id="rotate-view" class="view-panel">
  461. <div class="stack">
  462. <section class="section">
  463. <div class="section-head">
  464. <div class="section-title">🔁 Rotate / CPA 401</div>
  465. <div class="section-meta" id="rotate-meta">-</div>
  466. </div>
  467. <div class="section-body">
  468. <div id="rotate-grid" class="mini-grid"></div>
  469. <div id="rotate-log-wrap" class="compact-list"></div>
  470. </div>
  471. </section>
  472. </div>
  473. </section>
  474. <section id="proxy-view" class="view-panel">
  475. <div class="stack">
  476. <section class="section">
  477. <div class="section-head">
  478. <div class="section-title">🌐 代理池</div>
  479. <div class="section-meta" id="proxy-meta">-</div>
  480. </div>
  481. <div class="section-body">
  482. <div id="proxy-wrap" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:4px 16px"></div>
  483. </div>
  484. </section>
  485. </div>
  486. </section>
  487. <section id="storage-view" class="view-panel">
  488. <div class="two-col">
  489. <section class="section">
  490. <div class="section-head">
  491. <div class="section-title">💾 写盘记录</div>
  492. <div class="section-meta" id="file-meta">-</div>
  493. </div>
  494. <div class="section-body compact-scroll" id="file-wrap"></div>
  495. </section>
  496. <section class="section">
  497. <div class="section-head">
  498. <div class="section-title">✅ 最近成功记录</div>
  499. <div class="section-meta" id="storage-success-meta">-</div>
  500. </div>
  501. <div class="section-body compact-scroll" id="storage-success-wrap"></div>
  502. </section>
  503. </div>
  504. </section>
  505. <section id="survival-view" class="view-panel">
  506. <div class="stack">
  507. <section class="section">
  508. <div class="section-head">
  509. <div class="section-title">🧪 固定 cohort 存活实验</div>
  510. <div style="display:flex;align-items:center;gap:10px">
  511. <button id="survival-reseed-btn" class="btn-inline" type="button">重置为最近10个</button>
  512. <div class="section-meta" id="survival-meta">-</div>
  513. </div>
  514. </div>
  515. <div class="section-body">
  516. <div id="survival-grid" class="mini-grid"></div>
  517. <div id="survival-changes-wrap" class="compact-list" style="margin-top:12px"></div>
  518. </div>
  519. </section>
  520. <section class="section">
  521. <div class="section-head">
  522. <div class="section-title">📋 固定 cohort 16 个明细</div>
  523. <div class="section-meta" id="survival-members-meta">-</div>
  524. </div>
  525. <div class="stream" id="survival-members-wrap"></div>
  526. </section>
  527. <section class="section">
  528. <div class="section-head">
  529. <div class="section-title">🛰️ 新建 8 号 Unauthorized 追踪</div>
  530. <div class="section-meta" id="fresh401-meta">-</div>
  531. </div>
  532. <div class="section-body">
  533. <div id="fresh401-grid" class="mini-grid"></div>
  534. <div class="stream" id="fresh401-wrap" style="margin-top:12px"></div>
  535. </div>
  536. </section>
  537. </div>
  538. </section>
  539. <section id="settings-view" class="view-panel">
  540. <div class="stack">
  541. <section class="settings-card">
  542. <div class="section-head">
  543. <div class="section-title">⚙️ Settings</div>
  544. <div class="section-meta">当前模式 <span id="settings-mode">-</span></div>
  545. </div>
  546. <div class="section-body">
  547. <div class="settings-feedback" id="settings-feedback"></div>
  548. <div class="settings-grid" style="margin-top:12px">
  549. <section class="settings-card">
  550. <div class="section-head">
  551. <div class="section-title">注册机控制</div>
  552. <div class="settings-status-row">
  553. <span id="settings-register-state" class="status-pill warn">已停止</span>
  554. </div>
  555. </div>
  556. <div class="section-body">
  557. <div class="control-row" style="margin-bottom:12px">
  558. <button class="btn-inline" type="button" data-register-action="start">启动</button>
  559. <button class="btn-inline btn-danger" type="button" data-register-action="stop">停止</button>
  560. <button class="btn-inline" type="button" data-register-action="restart">重启</button>
  561. </div>
  562. <div class="form-grid">
  563. <div class="field">
  564. <label for="settings-register-threads">线程数</label>
  565. <input id="settings-register-threads" class="input" type="number" min="1">
  566. </div>
  567. <div class="field">
  568. <label for="settings-register-provider">邮箱提供商</label>
  569. <input id="settings-register-provider" class="input" type="text">
  570. </div>
  571. <div class="field">
  572. <label for="settings-register-batch-target">批次目标</label>
  573. <input id="settings-register-batch-target" class="input" type="number" min="1">
  574. </div>
  575. <div class="field">
  576. <label for="settings-register-batch-interval">批次间隔(秒)</label>
  577. <input id="settings-register-batch-interval" class="input" type="number" min="60">
  578. </div>
  579. <div class="field full">
  580. <label for="settings-register-proxy">注册代理</label>
  581. <input id="settings-register-proxy" class="input" type="text">
  582. </div>
  583. </div>
  584. </div>
  585. </section>
  586. <section class="settings-card">
  587. <div class="section-head">
  588. <div class="section-title">代理池</div>
  589. <div class="section-meta" id="settings-proxy-meta">-</div>
  590. </div>
  591. <div class="section-body">
  592. <div class="form-grid">
  593. <div class="field full">
  594. <label for="settings-proxy-direct-urls">代理 URL 列表</label>
  595. <textarea id="settings-proxy-direct-urls" class="textarea"></textarea>
  596. </div>
  597. <div class="field">
  598. <label for="settings-proxy-size">池大小</label>
  599. <input id="settings-proxy-size" class="input" type="number" min="1">
  600. </div>
  601. <div class="field">
  602. <label for="settings-proxy-regions">优先地区</label>
  603. <input id="settings-proxy-regions" class="input" type="text">
  604. </div>
  605. </div>
  606. </div>
  607. </section>
  608. <section class="settings-card">
  609. <div class="section-head">
  610. <div class="section-title">cfmail</div>
  611. <div class="section-meta" id="settings-cfmail-meta">-</div>
  612. </div>
  613. <div class="section-body">
  614. <div id="settings-cfmail-grid" class="kv-grid"></div>
  615. </div>
  616. </section>
  617. <section class="settings-card" data-cpa-only>
  618. <div class="section-head">
  619. <div class="section-title">CPA / 主池</div>
  620. <div class="section-meta" id="settings-cpa-meta">-</div>
  621. </div>
  622. <div class="section-body">
  623. <div class="form-grid">
  624. <div class="field">
  625. <label for="settings-cpa-main-pool-target">主池目标</label>
  626. <input id="settings-cpa-main-pool-target" class="input" type="number" min="1">
  627. </div>
  628. <div class="field">
  629. <label for="settings-cpa-rotate-interval">Rotate 间隔(秒)</label>
  630. <input id="settings-cpa-rotate-interval" class="input" type="number" min="1">
  631. </div>
  632. </div>
  633. </div>
  634. </section>
  635. </div>
  636. <section class="settings-card" style="margin-top:16px">
  637. <div class="section-head">
  638. <div class="section-title">依赖状态</div>
  639. <div class="section-meta">每 30s 自动刷新</div>
  640. </div>
  641. <div class="section-body">
  642. <div class="dep-grid">
  643. <div class="dep-item">
  644. <div class="dep-copy"><strong>cfmail</strong><span id="dep-cfmail-detail">-</span></div>
  645. <div id="dep-cfmail-status" class="status-pill warn">未加载</div>
  646. </div>
  647. <div class="dep-item">
  648. <div class="dep-copy"><strong>代理池</strong><span id="dep-proxy-detail">-</span></div>
  649. <div id="dep-proxy-status" class="status-pill warn">未加载</div>
  650. </div>
  651. <div class="dep-item" data-cpa-only>
  652. <div class="dep-copy"><strong>CPA</strong><span id="dep-cpa-detail">-</span></div>
  653. <div id="dep-cpa-status" class="status-pill warn">未加载</div>
  654. </div>
  655. </div>
  656. </div>
  657. </section>
  658. <div class="settings-actions" style="margin-top:16px">
  659. <button id="settings-save-btn" class="btn-inline" type="button">保存配置</button>
  660. </div>
  661. </div>
  662. </section>
  663. </div>
  664. </section>
  665. </div>
  666. </div>
  667. </div>
  668. <script>
  669. (()=>{
  670. const $=s=>document.getElementById(s);
  671. const esc=s=>String(s??"").replace(/[&<>"]/g,m=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[m]));
  672. const num=v=>Number.isFinite(+v)?(+v).toLocaleString("zh-CN"):"-";
  673. const pct=v=>Number.isFinite(+v)?v+"%":"-";
  674. const compact=v=>{const n=+v;if(!Number.isFinite(n))return"-";if(n>=1e9)return(n/1e9).toFixed(1)+"B";if(n>=1e6)return(n/1e6).toFixed(1)+"M";if(n>=1e4)return(n/1e3).toFixed(0)+"K";return n.toLocaleString("zh-CN")};
  675. const dt=v=>{if(!v)return"-";const d=new Date(v);return isNaN(+d)?String(v):d.toLocaleString("zh-CN",{hour12:false})};
  676. const ago=v=>{if(!v)return"-";const s=Math.max(0,Math.round((Date.now()-new Date(v))/1000));if(!Number.isFinite(s))return"-";if(s<60)return s+"s";if(s<3600)return Math.floor(s/60)+"m";return Math.floor(s/3600)+"h"};
  677. const nameMap={register:"注册循环",cleanup:"池清理",validate:"额度验证",rotate:"主池轮换",account_survival:"存活实验"};
  678. const viewMap={
  679. "overview":"overview-view",
  680. "active-domain":"active-domain-view",
  681. "cumulative":"cumulative-view",
  682. "rotate":"rotate-view",
  683. "proxy":"proxy-view",
  684. "storage":"storage-view",
  685. "survival":"survival-view",
  686. };
  687. let busy=0, settingsBusy=0, lastErr="";
  688. let latestSummary=null, latestSettings=null, latestDependencies=null;
  689. async function fetchJson(url){
  690. const ctl=new AbortController();
  691. const t=setTimeout(()=>ctl.abort(),15000);
  692. try{
  693. const r=await fetch(url,{cache:"no-store",signal:ctl.signal});
  694. if(!r.ok) throw new Error("HTTP "+r.status);
  695. return await r.json();
  696. }finally{
  697. clearTimeout(t);
  698. }
  699. }
  700. async function grab(){
  701. return fetchJson(`${location.origin}/api/summary`);
  702. }
  703. function heroCard(label, value, sub, colorClass){
  704. return `<div class="hero-card ${colorClass}">
  705. <div class="hero-label">${label}</div>
  706. <div class="hero-value">${value}</div>
  707. <div class="hero-sub">${sub}</div>
  708. </div>`;
  709. }
  710. function currentHashView(){
  711. const raw=String(location.hash||"").replace(/^#/,"").trim();
  712. return viewMap[raw]?raw:"overview";
  713. }
  714. function setActiveView(view,{updateHash=true}={}){
  715. const normalized=viewMap[view]?view:"overview";
  716. document.querySelectorAll(".nav-btn").forEach(btn=>{
  717. btn.classList.toggle("active", btn.dataset.view===normalized);
  718. });
  719. Object.entries(viewMap).forEach(([key,id])=>{
  720. const el=$(id);
  721. if(el) el.classList.toggle("active", key===normalized);
  722. });
  723. if(updateHash){
  724. const nextHash=`#${normalized}`;
  725. if(location.hash!==nextHash){
  726. history.replaceState(null,"",nextHash);
  727. }
  728. }
  729. }
  730. function statusTone(status){
  731. const normalized=String(status||"").toLowerCase();
  732. if(normalized==="ok" || normalized==="started" || normalized==="running" || normalized==="已运行" || normalized==="运行中") return "ok";
  733. if(normalized==="error" || normalized==="stopped" || normalized==="已停止") return "error";
  734. return "warn";
  735. }
  736. function setStatusPill(id, text, tone){
  737. const el=$(id);
  738. if(!el) return;
  739. el.textContent=text;
  740. el.className=`status-pill ${tone||statusTone(text)}`;
  741. }
  742. function applyModeVisibility(mode){
  743. const lite=String(mode||"").toLowerCase()==="lite";
  744. document.querySelectorAll("[data-cpa-only]").forEach(el=>{
  745. el.style.display=lite?"none":"";
  746. });
  747. if(lite && currentHashView()==="rotate"){
  748. setActiveView("overview");
  749. }
  750. }
  751. function showSettingsFeedback(message, tone="ok"){
  752. const el=$("settings-feedback");
  753. if(!el) return;
  754. if(!message){
  755. el.textContent="";
  756. el.className="settings-feedback";
  757. return;
  758. }
  759. el.textContent=message;
  760. el.className=`settings-feedback ${tone}`;
  761. }
  762. function render(d){
  763. const rt=d.runtime||{}, ra=d.regular_accounts||{}, pp=rt.proxy_pool||{};
  764. const tasks=rt.task_states||d.task_states||[];
  765. const mode=String((latestSettings&&latestSettings.mode)||rt.runtime_mode||"full");
  766. const isLite=mode==="lite";
  767. const files=d.recent_pool_files||[];
  768. const reg=tasks.find(t=>t.name==="register")||{};
  769. const rotateTask=d.rotate_task||tasks.find(t=>t.name==="rotate")||{};
  770. const rotateLog=d.rotate_log_tail||{};
  771. const rotateLiveSummary=d.rotate_current_summary||rotateLog.current_summary||{};
  772. const rotateLatestSummary=d.rotate_latest_summary||rotateLog.latest_summary||{};
  773. const rotateSummary=(rotateTask.is_running&&Object.keys(rotateLiveSummary).length)?rotateLiveSummary:rotateLatestSummary;
  774. const runtimeState=d.runtime_state_file||{};
  775. const burstPlan=d.register_burst_plan||{};
  776. const survival=d.account_survival||{};
  777. const survivalSummary=survival.summary||{};
  778. const survivalMembers=Array.isArray(survival.members)?survival.members:[];
  779. const survivalChanges=Array.isArray(survival.changes)?survival.changes:[];
  780. const fresh401=survival.fresh_unauthorized_experiment||{};
  781. const fresh401Summary=fresh401.summary||{};
  782. const fresh401Members=Array.isArray(fresh401.members)?fresh401.members:[];
  783. const registerWorkerThreads=Number.isFinite(+reg.register_worker_threads)?+reg.register_worker_threads:Math.max(0,(+reg.threads_total||0)-1);
  784. const retrySidecarThreads=Number.isFinite(+reg.retry_sidecar_threads)?+reg.retry_sidecar_threads:((+reg.threads_total||0)>0?1:0);
  785. const currentWarmupBacklog=Number.isFinite(+reg.current_warmup_backlog)?+reg.current_warmup_backlog:0;
  786. const stoploss=d.register_cfmail_add_phone_stoploss||{};
  787. const domainPool=((reg.cfmail_domain_pool||{}).active_domains||[]);
  788. const selectedDomainRow=domainPool[0]||null;
  789. const activeDomain=((selectedDomainRow&&selectedDomainRow.domain)||((reg.cfmail_rotation||{}).active_domain||stoploss.active_domain||"")).trim();
  790. const attempts=d.register_recent_attempts||d.recent_attempts||[];
  791. const activeAttempts=(reg.active_domain_recent_attempts&&reg.active_domain_recent_attempts.length)
  792. ? reg.active_domain_recent_attempts
  793. : (activeDomain ? attempts.filter(a=>(a.email_domain||"")===activeDomain) : attempts);
  794. const activeFailStage=(activeDomain ? (reg.active_domain_failure_by_stage||{}) : (reg.failure_by_stage||{}));
  795. const activeFailSignal=(activeDomain ? (reg.active_domain_failure_signals||{}) : (reg.failure_signals||{}));
  796. const activeFailHotspots=(activeDomain ? (reg.active_domain_recent_failure_hotspots||[]) : (reg.recent_failure_hotspots||[]));
  797. const cumulativeFailStage=reg.failure_by_stage||{};
  798. const cumulativeFailSignal=reg.failure_signals||{};
  799. applyModeVisibility(mode);
  800. // Status
  801. const regScheduled=String(reg.status||"").toLowerCase()==="scheduled";
  802. const ok=(reg.is_running && reg.status==="running") || regScheduled;
  803. $("pulse").className="pulse"+(ok?"":" bad");
  804. $("status-text").textContent=regScheduled?"SCHEDULED":(ok?"RUNNING":"ATTENTION");
  805. $("status-text").className="status-text"+(ok?"":" bad");
  806. $("updated").textContent=`更新: ${dt(d.generated_at)} · provider: ${esc(reg.mail_provider||"-")} · ${num(registerWorkerThreads)} worker + ${num(retrySidecarThreads)} retry`;
  807. // Hide alert on success
  808. $("alert").style.display="none";
  809. // Hero metrics
  810. const registeredSuccess=Number.isFinite(+d.registered_success_total)?+d.registered_success_total:(Number.isFinite(+reg.total_success_registered)?+reg.total_success_registered:+(reg.total_success||0));
  811. const registeredRate=reg.total_attempts>0?((registeredSuccess/reg.total_attempts)*100).toFixed(1):0;
  812. const activeSuccessCount=activeAttempts.filter(a=>a.success).length;
  813. const activeAttemptCount=activeAttempts.length;
  814. const activeSuccessRate=activeAttemptCount>0?((activeSuccessCount/activeAttemptCount)*100).toFixed(1):null;
  815. const mins=reg.last_started_at?Math.max(1,(Date.now()-new Date(reg.last_started_at))/60000):0;
  816. const speed=mins>0&&registeredSuccess>0?(registeredSuccess/mins).toFixed(1):"-";
  817. const heroCards=[
  818. heroCard("累计尝试 / 成功", `${num(reg.total_attempts||0)} / ${num(registeredSuccess)}`, `速度 ${speed}/min · 成功率 ${pct(registeredRate)}`, "c-green"),
  819. ];
  820. heroCards.push(
  821. heroCard("活动域池", num((reg.cfmail_domain_pool||{}).active_count||domainPool.length), `目标 ${num((reg.cfmail_domain_pool||{}).target_count||0)} · replenishing ${((reg.cfmail_domain_pool||{}).replenishing)?"yes":"no"}`, "c-blue"),
  822. );
  823. heroCards.push(
  824. heroCard("Warmup", `${num(currentWarmupBacklog)} backlog`, `累计 ${num(reg.total_warmup_pending||0)} 次`, "c-amber"),
  825. );
  826. if(!isLite){
  827. heroCards.push(
  828. heroCard("主池", `${num(d.cpa_count)}`, `本地 Pool ${num(d.pool_count)}`, "c-blue"),
  829. heroCard("可用 Tokens", compact((d.tokens||{}).available_now), `单号 ${compact((d.tokens||{}).per_account)} · ${num((d.tokens||{}).relevant_accounts)} 号`, "c-purple"),
  830. heroCard("可用 / 失效", `${num(ra.available)} / ${num(ra.invalid)}`, `待重置 ${num(d.observed_loss)}`, "c-amber"),
  831. );
  832. }else{
  833. heroCards.push(
  834. heroCard("主池", num(d.cpa_count), `本地 Pool ${num(d.pool_count)}`, "c-blue"),
  835. heroCard("代理池", num(pp.node_count||0), `${num(pp.in_use_count||0)} 使用中`, "c-purple"),
  836. );
  837. }
  838. heroCards.push(heroCard("今日新增", num(d.today_new), `本地 Pool ${num(d.pool_count)}`, "c-green"));
  839. $("hero").innerHTML=heroCards.join("");
  840. function failBars(obj, total){
  841. return Object.entries(obj).sort((a,b)=>b[1]-a[1]).map(([k,v])=>{
  842. const w=Math.round((v/total)*100);
  843. return `<div class="fail-bar-wrap">
  844. <div class="fail-bar-label"><span class="name">${esc(k)}</span><span class="count">${num(v)} (${w}%)</span></div>
  845. <div class="fail-bar"><div class="fail-bar-fill" style="width:${w}%"></div></div>
  846. </div>`;
  847. }).join("");
  848. }
  849. function attemptTable(rows){
  850. if(!rows.length) return '<div class="empty">暂无记录</div>';
  851. return `<table><thead><tr>
  852. <th>时间</th><th>阶段</th><th>信号</th><th>代理</th><th>域名</th>
  853. </tr></thead><tbody>${rows.map(a=>{
  854. const bc=a.success?"badge-ok":"badge-fail";
  855. const sig=a.signal||a.create_account_error_code||(a.success?"✅":"FAIL");
  856. return `<tr>
  857. <td class="mono">${dt(a.timestamp).split(" ").pop()||"-"}</td>
  858. <td><span class="badge ${bc}">${esc(a.stage||"-")}</span></td>
  859. <td style="font-size:11px">${esc(sig)}</td>
  860. <td style="font-size:11px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(a.proxy_key||"direct")}</td>
  861. <td class="mono" style="font-size:10px;color:var(--muted)">${esc(a.email_domain||"-")}</td>
  862. </tr>`;
  863. }).join("")}</tbody></table>`;
  864. }
  865. function successList(rows){
  866. if(!rows.length) return '<div class="empty">暂无成功记录</div>';
  867. return rows.map(a=>`<div class="file-item">
  868. <div class="file-name" style="color:var(--good)">✅ ${esc(a.email||a.email_domain||"-")}</div>
  869. <div class="file-meta">${esc(a.proxy_key||"direct")} · ${dt(a.timestamp).split(" ").pop()||"-"}</div>
  870. </div>`).join("");
  871. }
  872. // Active domain analysis
  873. const activeFailCount=Object.values(activeFailStage).reduce((a,b)=>a+b,0);
  874. const activeSigCount=Object.values(activeFailSignal).reduce((a,b)=>a+b,0);
  875. const activeSuccesses=activeAttempts.filter(a=>a.success);
  876. const activeVisibleAttempts=activeAttempts.slice(-40);
  877. const activeVisibleSuccesses=activeSuccesses.slice(-24).reverse();
  878. $("active-domain-name").textContent=domainPool.length ? `${num(domainPool.length)} 个活动域` : (activeDomain||"-");
  879. $("active-domain-sub").textContent=domainPool.length
  880. ? `目标 ${num((reg.cfmail_domain_pool||{}).target_count||0)} 个 · 当前主展示域 ${activeDomain||"-"}`
  881. : (activeDomain
  882. ? `最近窗口 ${activeAttempts.length} 条域内记录 · 成功率 ${activeSuccessRate===null?"-":pct(activeSuccessRate)} · 失败 ${num(activeFailCount)}`
  883. : "当前还没有可识别的活跃域");
  884. $("active-domain-meta").textContent=domainPool.length
  885. ? `${((reg.cfmail_domain_pool||{}).replenishing)?"补位中":"稳定"} · ${((reg.cfmail_domain_pool||{}).replenish_reason)||"-"}`
  886. : (activeDomain ? "仅看当前域" : "等待域名数据");
  887. $("active-domain-kpis").innerHTML=domainPool.length
  888. ? domainPool.map(item=>heroMini(
  889. item.domain||"-",
  890. `${num(item.recent_success||0)} / ${num(item.recent_attempts||0)}`,
  891. [
  892. `inflight ${num(item.inflight||0)}`,
  893. `fail ${num(item.recent_failure||0)}`,
  894. item.add_phone_cooldown?"add_phone cooldown":"",
  895. item.wait_otp_cooldown?"wait_otp cooldown":"",
  896. (item.start_interval_remaining_seconds||0)>0?`start ${num(item.start_interval_remaining_seconds)}s`:"",
  897. (item.skip_remaining_seconds||0)>0?`skip ${num(item.skip_remaining_seconds)}s`:"",
  898. ].filter(Boolean).join(" · ")||"ready"
  899. )).join("")
  900. : [
  901. heroMini("窗口成功率", activeSuccessRate===null?"-":pct(activeSuccessRate), `样本 ${num(activeAttempts.length)}`),
  902. heroMini("窗口尝试", num(activeAttempts.length), `成功 ${num(activeSuccesses.length)}`),
  903. heroMini("窗口失败", num(activeFailCount), `信号 ${num(activeSigCount)}`),
  904. heroMini("最近成功", num(activeVisibleSuccesses.length), activeVisibleSuccesses[0]?.email||"-"),
  905. heroMini("热点", activeFailHotspots[0]?.key||"-", activeFailHotspots[0]?`次数 ${num(activeFailHotspots[0].count)}`:"暂无"),
  906. heroMini("最近记录", num(activeVisibleAttempts.length), activeDomain||"-"),
  907. heroMini("轮换状态", ((reg.cfmail_rotation||{}).in_progress)?"进行中":"稳定", ((reg.cfmail_rotation||{}).last_reason)||"-"),
  908. ].join("");
  909. $("active-fail-domain-name").textContent=activeDomain||"-";
  910. $("active-fail-domain-sub").textContent=activeDomain
  911. ? `当前活跃域专属统计 · 最近 ${activeVisibleAttempts.length} 条`
  912. : "暂无活跃域数据";
  913. const activeFailTotal=Object.values(activeFailStage).reduce((a,b)=>a+b,0)||1;
  914. const activeSigTotal=Object.values(activeFailSignal).reduce((a,b)=>a+b,0)||1;
  915. $("fail-meta").textContent=activeDomain
  916. ? `${esc(activeDomain)} · ${num(activeFailCount)} 次失败`
  917. : `${num(reg.total_failure)} 次失败`;
  918. $("fail-grid").innerHTML=`
  919. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按阶段</div>${failBars(activeFailStage,activeFailTotal)||'<div class="empty">暂无</div>'}</div>
  920. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按信号</div>${failBars(activeFailSignal,activeSigTotal)||'<div class="empty">暂无</div>'}</div>
  921. `;
  922. $("stream-meta").textContent=activeDomain
  923. ? `${esc(activeDomain)} · 最近 ${activeVisibleAttempts.length} / ${activeAttempts.length} 条`
  924. : `最近 ${activeVisibleAttempts.length} / ${attempts.length} 条`;
  925. $("stream-wrap").innerHTML=attemptTable(activeVisibleAttempts);
  926. $("success-meta").textContent=activeDomain
  927. ? `${esc(activeDomain)} · ${activeVisibleSuccesses.length} / ${activeSuccesses.length} 条成功`
  928. : `${activeVisibleSuccesses.length} / ${activeSuccesses.length} 条成功`;
  929. $("success-wrap").innerHTML=successList(activeVisibleSuccesses);
  930. // Cumulative analysis
  931. const cumulativeFailCount=Object.values(cumulativeFailStage).reduce((a,b)=>a+b,0);
  932. const cumulativeSigCount=Object.values(cumulativeFailSignal).reduce((a,b)=>a+b,0);
  933. const cumulativeSuccesses=attempts.filter(a=>a.success);
  934. const cumulativeVisibleAttempts=attempts.slice(-40);
  935. const cumulativeFailTotal=Object.values(cumulativeFailStage).reduce((a,b)=>a+b,0)||1;
  936. const cumulativeSigTotal=Object.values(cumulativeFailSignal).reduce((a,b)=>a+b,0)||1;
  937. $("overview-fail-meta").textContent=`累计失败 ${num(cumulativeFailCount)} 次`;
  938. $("overview-fail-grid").innerHTML=`
  939. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按阶段</div>${failBars(cumulativeFailStage,cumulativeFailTotal)||'<div class="empty">暂无</div>'}</div>
  940. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按信号</div>${failBars(cumulativeFailSignal,cumulativeSigTotal)||'<div class="empty">暂无</div>'}</div>
  941. `;
  942. $("cumulative-meta").textContent=`启动至今 ${num(reg.total_attempts||0)} 次尝试`;
  943. $("cumulative-grid").innerHTML=[
  944. heroMini("总尝试", num(reg.total_attempts||0), `注册成功 ${num(registeredSuccess)}`),
  945. heroMini("总失败", num(reg.total_failure||0), `成功率 ${pct(registeredRate)}`),
  946. heroMini("当前速度", speed==="-"?"-":`${speed}/min`, reg.last_started_at?`started ${dt(reg.last_started_at).split(" ").pop()||"-"}`:"-"),
  947. heroMini("Pool 备份", num(d.pool_count), `CPA 主池 ${num(d.cpa_count)}`),
  948. heroMini("活动域池", num((reg.cfmail_domain_pool||{}).active_count||domainPool.length), `目标 ${num((reg.cfmail_domain_pool||{}).target_count||0)}`),
  949. heroMini("最近热点", (reg.recent_failure_hotspots||[])[0]?.key||"-", (reg.recent_failure_hotspots||[])[0]?`次数 ${num((reg.recent_failure_hotspots||[])[0].count)}`:"暂无"),
  950. ].join("");
  951. $("cumulative-fail-grid").innerHTML=`
  952. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按阶段</div>${failBars(cumulativeFailStage,cumulativeFailTotal)||'<div class="empty">暂无</div>'}</div>
  953. <div><div style="font-size:11px;font-weight:600;color:var(--muted);margin-bottom:8px">按信号</div>${failBars(cumulativeFailSignal,cumulativeSigTotal)||'<div class="empty">暂无</div>'}</div>
  954. `;
  955. $("cumulative-stream-meta").textContent=`最近 ${cumulativeVisibleAttempts.length} / ${attempts.length} 条`;
  956. $("cumulative-stream-wrap").innerHTML=attemptTable(cumulativeVisibleAttempts);
  957. // Tasks
  958. const ordered=(isLite?["register"]:["register","rotate","cleanup","validate","account_survival"]).map(n=>tasks.find(t=>t.name===n)||{name:n,status:"idle"});
  959. $("task-meta").textContent=ordered.filter(t=>/running/i.test(t.status)).length+" 运行中";
  960. $("tasks").innerHTML=ordered.map(t=>{
  961. const sc=/running/i.test(t.status||"")?"running":/fail|error|stop/i.test(t.status||"")?"error":"idle";
  962. const extra=t.name==="register"
  963. ? ((t.next_run_at && !/running/i.test(t.status||"")) ? `下次 ${ago(t.next_run_at)}` : `worker ${num(t.register_worker_threads||Math.max(0,(+t.threads_total||0)-1))} + retry ${num(t.retry_sidecar_threads||((+t.threads_total||0)>0?1:0))}`)
  964. : (t.next_run_at?`下次 ${ago(t.next_run_at)}`:"");
  965. return `<div class="task-card">
  966. <div class="task-name">${esc(nameMap[t.name]||t.name)}</div>
  967. <span class="task-status ${sc}">${esc(t.status||"idle")}</span>
  968. <div class="task-stats">
  969. <span>运行 ${num(t.run_count)}</span><span>成功 ${num(t.success_count)}</span>
  970. <span>失败 ${num(t.failure_count)}</span><span>${extra}</span>
  971. </div>
  972. </div>`;
  973. }).join("");
  974. // Rotate / CPA 401
  975. const rotateEvents=rotateLog.recent_events||[];
  976. const rotateStatus=rotateTask.status||"-";
  977. const rotateRuns=Number.isFinite(+rotateTask.run_count)?`运行 ${num(rotateTask.run_count)}`:"-";
  978. const rotateElapsedSeconds=rotateTask.is_running&&rotateTask.last_started_at
  979. ? Math.max(0, Math.round((Date.now()-new Date(rotateTask.last_started_at).getTime())/1000))
  980. : null;
  981. const rotateMetaParts=[
  982. rotateTask.is_running?"执行中":rotateStatus,
  983. rotateTask.last_finished_at?`上次 ${dt(rotateTask.last_finished_at).split(" ").pop()||"-"}`:"",
  984. !rotateTask.is_running&&rotateTask.next_run_at?`下次 ${dt(rotateTask.next_run_at).split(" ").pop()||"-"}`:"",
  985. rotateRuns,
  986. ].filter(Boolean);
  987. $("rotate-meta").textContent=rotateMetaParts.join(" · ")||"-";
  988. const rotateMainAfter=rotateTask.is_running
  989. ? (d.cpa_count ?? rotateSummary.main_after ?? null)
  990. : (rotateSummary.main_after ?? d.cpa_count ?? null);
  991. const rotateMainBefore=rotateSummary.main_before ?? rotateMainAfter ?? null;
  992. const rotateProbeSkipped=rotateSummary.quota_probe_skipped ?? 0;
  993. const rotateProbe429=rotateSummary.quota_probe_429 ?? 0;
  994. const rotateDurationText=rotateTask.is_running
  995. ? `${num(rotateElapsedSeconds)}s`
  996. : `${rotateTask.last_duration_seconds ?? "-"}s`;
  997. $("rotate-grid").innerHTML=[
  998. heroMini("401删除", num(rotateSummary.deleted_401), `主池 ${num(rotateMainBefore)} → ${num(rotateMainAfter)}`),
  999. heroMini("quota探测", num(rotateSummary.quota_probed), `probe401 ${num(rotateSummary.quota_probe_401)}`),
  1000. heroMini("probe429", num(rotateProbe429), `probe跳过 ${num(rotateProbeSkipped)}`),
  1001. heroMini("本轮耗时", rotateDurationText, `429 保留`),
  1002. heroMini("主池状态", num(rotateMainAfter), `当前 CPA ${num(d.cpa_count)}`),
  1003. ].join("");
  1004. $("rotate-log-wrap").innerHTML=rotateEvents.length
  1005. ? rotateEvents.slice(-10).reverse().map(line=>`<div class="compact-item">
  1006. <div class="line1">${esc((line.split("] ").slice(1).join("] "))||line)}</div>
  1007. <div class="line2">${esc(line.split("]")[0].replace("[","")||"-")}</div>
  1008. </div>`).join("")
  1009. : '<div class="empty">暂无 rotate 日志</div>';
  1010. // Storage / recent successes
  1011. const storageVisibleSuccesses=cumulativeSuccesses.slice(-24).reverse();
  1012. $("storage-success-meta").textContent=`${storageVisibleSuccesses.length} / ${cumulativeSuccesses.length} 条成功`;
  1013. $("storage-success-wrap").innerHTML=successList(storageVisibleSuccesses);
  1014. const nodes=pp.nodes||[];
  1015. $("proxy-meta").textContent=`${num(pp.node_count)} 节点 · ${num(pp.in_use_count)} 使用中`;
  1016. $("proxy-wrap").innerHTML=nodes.map(n=>{
  1017. const total=(n.successes||0)+(n.failures||0);
  1018. const ratio=total>0?Math.round((n.successes/total)*100):0;
  1019. const dotCls=n.disabled?"disabled":n.in_use?"active":"idle";
  1020. return `<div class="proxy-item">
  1021. <div class="proxy-dot ${dotCls}"></div>
  1022. <div class="proxy-name" title="${esc(n.name)}">${esc(n.name||"-")}</div>
  1023. <div class="proxy-bar"><div class="proxy-bar-fill" style="width:${ratio}%"></div></div>
  1024. <div class="proxy-ratio">${ratio}%</div>
  1025. </div>`;
  1026. }).join("")||'<div class="empty">暂无代理</div>';
  1027. // Files
  1028. $("file-meta").textContent=files[0]?`最近 ${ago(files[0].modified_at_iso)}`:"暂无";
  1029. const fmtSize=b=>{if(!Number.isFinite(+b))return"-";const kb=b/1024;return kb>=1024?(kb/1024).toFixed(1)+" MB":kb.toFixed(1)+" KB"};
  1030. $("file-wrap").innerHTML=files.slice(0,8).map(f=>`<div class="file-item">
  1031. <div class="file-name">${esc(f.name||"-")}</div>
  1032. <div class="file-meta"><strong>${fmtSize(f.size_bytes)}</strong> · ${dt(f.modified_at_iso)}</div>
  1033. </div>`).join("")||'<div class="empty">暂无记录</div>';
  1034. // Account survival
  1035. const survivalUpdated=survival.updated_at||survival.seeded_at||"";
  1036. const survivalSeedSource=survival.seed_source==="latest_generated_pool_files"?"最近生成样本":"固定历史样本";
  1037. const survivalProbeMode=survival.probe_mode==="responses"?"responses":"usage";
  1038. const survivalProbeLabel=survivalProbeMode==="responses"?"responses 探测":"usage 探测";
  1039. $("survival-meta").textContent=survival.available
  1040. ? `更新 ${dt(survivalUpdated)} · 固定 ${num(survivalSummary.tracked||0)} 个 · ${survivalSeedSource} · ${survivalProbeLabel}`
  1041. : "实验尚未初始化";
  1042. $("survival-grid").innerHTML=[
  1043. heroMini("追踪账户", num(survivalSummary.tracked), `alive ${num(survivalSummary.alive)}`),
  1044. heroMini("首次401", num(survivalSummary.first_invalid_count), `invalid ${num(survivalSummary.invalid)}`),
  1045. heroMini("传输异常", num(survivalSummary.transport_error), `suspicious ${num(survivalSummary.suspicious)}`),
  1046. heroMini("missing", num(survivalSummary.missing), `removed after 401 ${num(survivalSummary.removed_after_invalid)}`),
  1047. heroMini("固定样本", survival.seeded_at?dt(survival.seeded_at).split(" ").pop():"-", `${survivalSeedSource} · cohort ${num(survival.cohort_size)}`),
  1048. heroMini("状态文件", survival.available?"ready":"missing", survival.path||"-"),
  1049. ].join("");
  1050. const survivalReseedBtn=$("survival-reseed-btn");
  1051. if(survivalReseedBtn){
  1052. survivalReseedBtn.style.display=survivalProbeMode==="responses"?"none":"inline-flex";
  1053. }
  1054. $("survival-changes-wrap").innerHTML=survivalChanges.length
  1055. ? survivalChanges.slice(-10).reverse().map(item=>`<div class="compact-item">
  1056. <div class="line1">${esc(item.email||"-")} · ${esc(item.from||"-")} → ${esc(item.to||"-")}</div>
  1057. <div class="line2">${esc(dt(item.probed_at))}${(item.survival_text||item.survival_seconds!=null)?` · 存活 ${esc(item.survival_text||`${item.survival_seconds}s`)}`:""}</div>
  1058. </div>`).join("")
  1059. : '<div class="empty">目前还没有状态变化</div>';
  1060. $("survival-members-meta").textContent=survivalMembers.length
  1061. ? `${survivalMembers.length} 个固定账户`
  1062. : "暂无 cohort";
  1063. $("survival-members-wrap").innerHTML=survivalMembers.length
  1064. ? `<table><thead><tr>
  1065. <th>邮箱</th><th>创建</th><th>最近探测</th><th>结果</th><th>首次401</th><th>存活时长</th><th>次数</th>
  1066. </tr></thead><tbody>${survivalMembers.map(m=>`
  1067. <tr>
  1068. <td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(m.email||"-")}">${esc(m.email||"-")}</td>
  1069. <td class="mono">${esc(dt(m.created_at).split(" ").pop()||"-")}</td>
  1070. <td class="mono">${esc(dt(m.last_probe_at).split(" ").pop()||"-")}</td>
  1071. <td><span class="badge ${(m.state==='invalid_removed'||m.last_probe_category==='invalid')?'badge-fail':(m.last_probe_category==='normal'?'badge-ok':'badge-warn')}">${esc(m.state==='invalid_removed'?'invalid_removed':(m.last_probe_category||"never_probed"))}</span></td>
  1072. <td class="mono">${esc(dt(m.first_invalid_at).split(" ").pop()||"-")}</td>
  1073. <td class="mono">${esc(m.survival_text||(m.survival_seconds!=null?`${m.survival_seconds}s`:"-"))}</td>
  1074. <td class="mono">${esc(num(m.probe_count||0))}</td>
  1075. </tr>`).join("")}</tbody></table>`
  1076. : '<div class="empty">暂无固定追踪账户</div>';
  1077. $("fresh401-meta").textContent=fresh401.available
  1078. ? `开始 ${dt(fresh401.started_at)} · ${num(fresh401Summary.tracked||0)}/${num(fresh401.cohort_size||8)} 个`
  1079. : "尚未启动专项追踪";
  1080. $("fresh401-grid").innerHTML=[
  1081. heroMini("已纳入", `${num(fresh401Summary.tracked||0)} / ${num(fresh401.cohort_size||8)}`, fresh401.updated_at_iso?`更新 ${dt(fresh401.updated_at_iso).split(" ").pop()}`:"等待新号"),
  1082. heroMini("首次401", num(fresh401Summary.first_401_count||0), `pending ${num(fresh401Summary.pending||0)}`),
  1083. heroMini("已完成", num(fresh401Summary.completed||0), fresh401.path||"-"),
  1084. ].join("");
  1085. $("fresh401-wrap").innerHTML=fresh401Members.length
  1086. ? `<table><thead><tr>
  1087. <th>邮箱</th><th>创建</th><th>门型</th><th>代理</th><th>最近结果</th><th>首次401</th><th>时长</th><th>报错</th>
  1088. </tr></thead><tbody>${fresh401Members.map(m=>`
  1089. <tr>
  1090. <td style="max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(m.email||"-")}">${esc(m.email||"-")}</td>
  1091. <td class="mono">${esc(dt(m.created_at).split(" ").pop()||"-")}</td>
  1092. <td>${esc(m.registration_post_create_gate||"direct")}</td>
  1093. <td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(m.registration_proxy_key||"-")}">${esc(m.registration_proxy_key||"-")}</td>
  1094. <td><span class="badge ${m.last_status_code===401?'badge-fail':(m.last_status_code===200?'badge-ok':'badge-warn')}">${esc((m.last_status_code||"-")+" "+(m.last_category||""))}</span></td>
  1095. <td class="mono">${esc(dt(m.first_401_at).split(" ").pop()||"-")}</td>
  1096. <td class="mono">${esc(m.first_401_text||(m.first_401_seconds!=null?`${m.first_401_seconds}s`:"-"))}</td>
  1097. <td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(m.first_401_detail||m.last_detail||"-")}">${esc(m.first_401_detail||m.last_detail||"-")}</td>
  1098. </tr>`).join("")}</tbody></table>`
  1099. : '<div class="empty">等待本轮新创建成功账号加入追踪</div>';
  1100. // Runtime config
  1101. $("cfg-meta").textContent=runtimeState.updated_at_iso?`快照 ${ago(runtimeState.updated_at_iso)}`:"快照缺失";
  1102. $("cfg-wrap").innerHTML=[
  1103. kvItem("mail provider", reg.mail_provider||"-"),
  1104. kvItem("register proxy", reg.proxy||"none"),
  1105. kvItem("proxy pool", reg.proxy_pool_enabled?"enabled":"disabled"),
  1106. kvItem("runtime state", runtimeState.updated_at_iso||"missing"),
  1107. kvItem("state file", runtimeState.path||"-"),
  1108. kvItem("rotate interval", `${num(rt.rotate_interval||0)}s`),
  1109. ].join("");
  1110. // Burst registration plan
  1111. const burstIntervalSeconds=Number.isFinite(+burstPlan.interval_seconds)?+burstPlan.interval_seconds:0;
  1112. const burstIntervalText=burstIntervalSeconds>0
  1113. ? (burstIntervalSeconds%3600===0?`${num(burstIntervalSeconds/3600)}h`:`${(burstIntervalSeconds/3600).toFixed(1)}h`)
  1114. : "-";
  1115. $("plan-meta").textContent=burstPlan.mode==="burst"
  1116. ? `1 天约 ${num(burstPlan.accounts_per_day)} 个`
  1117. : "未配置";
  1118. $("plan-wrap").innerHTML=[
  1119. heroMini("模式", burstPlan.mode==="burst"?"批次注册":"-", "非 24h 常驻"),
  1120. heroMini("线程", num(burstPlan.threads), `单批 ${num(burstPlan.target_count)} 个`),
  1121. heroMini("批次间隔", burstIntervalText, `${num(burstPlan.batches_per_day)} 轮/天`),
  1122. heroMini("日产估算", num(burstPlan.accounts_per_day), `目标 ${compact(burstPlan.one_day_token_target)} tokens/day`),
  1123. heroMini("1 亿当日下限", num(burstPlan.accounts_needed_for_one_day_target), `单号周配额 ${compact(burstPlan.free_account_weekly_tokens)}`),
  1124. heroMini("1 亿可持续", num(burstPlan.accounts_needed_for_sustained_daily_target), `普号存活 ${esc(burstPlan.survival_window_hours||"-")}`),
  1125. ].join("");
  1126. if(latestSettings) renderSettings();
  1127. }
  1128. function heroMini(label, value, sub){
  1129. return `<div class="mini-card">
  1130. <div class="mini-label">${esc(label)}</div>
  1131. <div class="mini-value">${esc(value)}</div>
  1132. <div class="mini-sub">${esc(sub||"-")}</div>
  1133. </div>`;
  1134. }
  1135. function kvItem(key, value){
  1136. return `<div class="kv-item">
  1137. <div class="kv-key">${esc(key)}</div>
  1138. <div class="kv-val">${esc(value||"-")}</div>
  1139. </div>`;
  1140. }
  1141. function renderSettings(){
  1142. if(!latestSettings) return;
  1143. const settings=latestSettings;
  1144. const deps=latestDependencies||{};
  1145. const summary=latestSummary||{};
  1146. const rt=summary.runtime||{};
  1147. const regTask=(rt.task_states||[]).find(t=>t.name==="register")||{};
  1148. const regState=String(regTask.status||"").toLowerCase()==="scheduled" ? "已调度" : (settings.register.enabled ? "运行中" : "已停止");
  1149. const regTone=regState==="运行中" || regState==="已调度" ? "ok" : "warn";
  1150. applyModeVisibility(settings.mode);
  1151. $("settings-mode").textContent=settings.mode||"-";
  1152. setStatusPill("settings-register-state", regState, regTone);
  1153. $("settings-register-threads").value=settings.register.threads ?? "";
  1154. $("settings-register-provider").value=settings.register.mail_provider ?? "";
  1155. $("settings-register-batch-target").value=settings.register.batch_target_count ?? "";
  1156. $("settings-register-batch-interval").value=settings.register.batch_interval_seconds ?? "";
  1157. $("settings-register-proxy").value=settings.register.proxy ?? "";
  1158. $("settings-proxy-direct-urls").value=settings.proxy_pool.direct_urls ?? "";
  1159. $("settings-proxy-size").value=settings.proxy_pool.size ?? "";
  1160. $("settings-proxy-regions").value=settings.proxy_pool.regions ?? "";
  1161. $("settings-cpa-rotate-interval").value=settings.cpa.rotate_interval ?? "";
  1162. $("settings-proxy-meta").textContent=settings.proxy_pool.enabled?"已启用":"已关闭";
  1163. $("settings-cfmail-meta").textContent=settings.cfmail.configured?"已配置":"未配置";
  1164. $("settings-cpa-meta").textContent=settings.cpa.rotate_enabled?"rotate 已启用":"rotate 已关闭";
  1165. const cpaManageBtn=$("btn-cpa-manage");
  1166. if(cpaManageBtn){
  1167. const managementUrl=String(settings.cpa.management_url||"").trim();
  1168. cpaManageBtn.href=managementUrl||"#";
  1169. cpaManageBtn.style.pointerEvents=managementUrl?"auto":"none";
  1170. cpaManageBtn.style.opacity=managementUrl?"1":"0.5";
  1171. cpaManageBtn.setAttribute("aria-disabled", managementUrl?"false":"true");
  1172. }
  1173. $("settings-cfmail-grid").innerHTML=[
  1174. kvItem("configured", settings.cfmail.configured?"true":"false"),
  1175. kvItem("zone", settings.cfmail.zone_name||"-"),
  1176. kvItem("worker", settings.cfmail.worker_name||"-"),
  1177. kvItem("rotation window", settings.cfmail.rotation_window),
  1178. kvItem("blacklist threshold", settings.cfmail.rotation_blacklist_threshold),
  1179. kvItem("proxy config", settings.proxy_pool.config_path||"-"),
  1180. ].join("");
  1181. const depMap=[
  1182. ["cfmail", deps.cfmail, "dep-cfmail-status", "dep-cfmail-detail"],
  1183. ["proxy_pool", deps.proxy_pool, "dep-proxy-status", "dep-proxy-detail"],
  1184. ["cpa", deps.cpa, "dep-cpa-status", "dep-cpa-detail"],
  1185. ];
  1186. depMap.forEach(([name, payload, statusId, detailId])=>{
  1187. const info=payload||{};
  1188. let label="未加载";
  1189. let detail="-";
  1190. if(name==="proxy_pool" && info.total_nodes!=null){
  1191. detail=`${num(info.active_nodes)} / ${num(info.total_nodes)} 活跃`;
  1192. }else if(name==="cpa"){
  1193. detail=`management ${info.management_reachable?"ok":"fail"}${info.detail?` · ${info.detail}`:""}`;
  1194. }else{
  1195. detail=info.detail||"-";
  1196. }
  1197. if(info.status==="ok") label="在线";
  1198. else if(info.status==="error" || info.status==="unavailable") label="异常";
  1199. else if(info.status==="unconfigured") label="未配置";
  1200. setStatusPill(statusId, label, statusTone(info.status||label));
  1201. if($(detailId)) $(detailId).textContent=detail;
  1202. });
  1203. }
  1204. async function load(){
  1205. if(busy)return;
  1206. busy=1;
  1207. try{
  1208. const d=await grab();
  1209. latestSummary=d;
  1210. lastErr="";
  1211. $("alert").style.display="none";
  1212. render(d);
  1213. }catch(e){
  1214. const msg=String(e&&e.message?e.message:e);
  1215. if(msg!==lastErr){
  1216. $("alert").textContent="数据获取失败: "+msg;
  1217. $("alert").style.display="block";
  1218. lastErr=msg;
  1219. }
  1220. }finally{busy=0}
  1221. }
  1222. async function loadSettings(){
  1223. if(settingsBusy) return;
  1224. settingsBusy=1;
  1225. try{
  1226. const [settings, deps]=await Promise.all([
  1227. fetchJson(`${location.origin}/api/settings`),
  1228. fetchJson(`${location.origin}/api/health/dependencies`),
  1229. ]);
  1230. latestSettings=settings;
  1231. latestDependencies=deps;
  1232. renderSettings();
  1233. }catch(e){
  1234. const msg=String(e&&e.message?e.message:e);
  1235. showSettingsFeedback("Settings 加载失败: "+msg, "error");
  1236. }finally{
  1237. settingsBusy=0;
  1238. }
  1239. }
  1240. async function saveSettings(){
  1241. const payload={
  1242. "register.threads": Number($("settings-register-threads").value||1),
  1243. "register.batch_target_count": Number($("settings-register-batch-target").value||1),
  1244. "register.batch_interval_seconds": Number($("settings-register-batch-interval").value||60),
  1245. "register.mail_provider": $("settings-register-provider").value,
  1246. "register.proxy": $("settings-register-proxy").value,
  1247. "proxy_pool.size": Number($("settings-proxy-size").value||1),
  1248. "proxy_pool.direct_urls": $("settings-proxy-direct-urls").value,
  1249. "proxy_pool.regions": $("settings-proxy-regions").value,
  1250. };
  1251. if(latestSettings && latestSettings.mode!=="lite"){
  1252. payload["cpa.rotate_interval"]=Number($("settings-cpa-rotate-interval").value||1);
  1253. }
  1254. const r=await fetch(`${location.origin}/api/settings`,{
  1255. method:"PUT",
  1256. headers:{"Content-Type":"application/json"},
  1257. body:JSON.stringify(payload),
  1258. });
  1259. if(!r.ok) throw new Error("HTTP "+r.status);
  1260. latestSettings=await r.json();
  1261. renderSettings();
  1262. showSettingsFeedback(latestSettings.restart_required?"已保存. 部分配置需重启后完全生效.":"已保存.", "ok");
  1263. await load();
  1264. await loadSettings();
  1265. }
  1266. async function controlRegister(action){
  1267. const r=await fetch(`${location.origin}/api/control/register`,{
  1268. method:"POST",
  1269. headers:{"Content-Type":"application/json"},
  1270. body:JSON.stringify({action}),
  1271. });
  1272. if(!r.ok) throw new Error("HTTP "+r.status);
  1273. await r.json();
  1274. showSettingsFeedback(`注册循环已${action==="start"?"启动":(action==="stop"?"停止":"重启")}.`, "ok");
  1275. await load();
  1276. await loadSettings();
  1277. }
  1278. async function reseedSurvival(){
  1279. const btn=$("survival-reseed-btn");
  1280. if(!btn || btn.disabled) return;
  1281. const original=btn.textContent;
  1282. btn.disabled=true;
  1283. btn.textContent="重置中...";
  1284. try{
  1285. const r=await fetch(`${location.origin}/api/account-survival/reseed`,{method:"POST"});
  1286. if(!r.ok) throw new Error("HTTP "+r.status);
  1287. lastErr="";
  1288. $("alert").style.display="none";
  1289. await load();
  1290. }catch(e){
  1291. const msg=String(e&&e.message?e.message:e);
  1292. $("alert").textContent="存活实验重置失败: "+msg;
  1293. $("alert").style.display="block";
  1294. lastErr=msg;
  1295. }finally{
  1296. btn.disabled=false;
  1297. btn.textContent=original;
  1298. }
  1299. }
  1300. document.addEventListener("click", e=>{
  1301. const saveBtn=e.target.closest("#settings-save-btn");
  1302. if(saveBtn){
  1303. saveSettings().catch(err=>{
  1304. showSettingsFeedback("保存失败: "+String(err&&err.message?err.message:err), "error");
  1305. });
  1306. return;
  1307. }
  1308. const registerBtn=e.target.closest("[data-register-action]");
  1309. if(registerBtn){
  1310. controlRegister(registerBtn.dataset.registerAction||"").catch(err=>{
  1311. showSettingsFeedback("控制失败: "+String(err&&err.message?err.message:err), "error");
  1312. });
  1313. return;
  1314. }
  1315. const survivalBtn=e.target.closest("#survival-reseed-btn");
  1316. if(survivalBtn){
  1317. reseedSurvival();
  1318. return;
  1319. }
  1320. const btn=e.target.closest(".nav-btn");
  1321. if(!btn) return;
  1322. const view=btn.dataset.view;
  1323. if(view && viewMap[view]) setActiveView(view);
  1324. });
  1325. window.addEventListener("hashchange", ()=>{
  1326. setActiveView(currentHashView(), {updateHash:false});
  1327. });
  1328. setActiveView(currentHashView(), {updateHash:false});
  1329. load();
  1330. setInterval(load,5000);
  1331. })();
  1332. </script>
  1333. </body>
  1334. </html>