@seasonkoh/webaz 0.1.26 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
  9. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  13. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  15. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  17. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  18. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  19. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  20. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  22. package/dist/ledger.js +1 -1
  23. package/dist/pwa/admin-audit.js +38 -0
  24. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  25. package/dist/pwa/cf-origin-guard.js +33 -0
  26. package/dist/pwa/contract-fingerprint.js +1 -0
  27. package/dist/pwa/data/onboarding-cases.js +2 -2
  28. package/dist/pwa/data/onboarding-quiz.js +1 -1
  29. package/dist/pwa/economic-participation.js +2 -2
  30. package/dist/pwa/integration-contract.js +46 -4
  31. package/dist/pwa/internal/pv-settlement.js +12 -0
  32. package/dist/pwa/internal/wallet-signer.js +26 -0
  33. package/dist/pwa/public/app.js +679 -679
  34. package/dist/pwa/public/i18n.js +15 -28
  35. package/dist/pwa/public/index.html +1 -1
  36. package/dist/pwa/public/openapi.json +4760 -2769
  37. package/dist/pwa/pv-kill-switch.js +31 -0
  38. package/dist/pwa/routes/admin-admins.js +48 -1
  39. package/dist/pwa/routes/admin-analytics.js +1 -10
  40. package/dist/pwa/routes/admin-atomic.js +4 -17
  41. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  42. package/dist/pwa/routes/admin-reports.js +4 -26
  43. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  44. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  45. package/dist/pwa/routes/admin-users-query.js +23 -1
  46. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  47. package/dist/pwa/routes/auth-read.js +1 -5
  48. package/dist/pwa/routes/auth-register.js +3 -13
  49. package/dist/pwa/routes/build-task-quota.js +113 -0
  50. package/dist/pwa/routes/claim-verify.js +15 -11
  51. package/dist/pwa/routes/contribution-facts.js +18 -0
  52. package/dist/pwa/routes/dispute-cases.js +5 -4
  53. package/dist/pwa/routes/growth.js +3 -3
  54. package/dist/pwa/routes/orders-action.js +27 -10
  55. package/dist/pwa/routes/orders-create.js +1 -1
  56. package/dist/pwa/routes/products-meta.js +19 -6
  57. package/dist/pwa/routes/profile-placement.js +1 -1
  58. package/dist/pwa/routes/promoter.js +10 -29
  59. package/dist/pwa/routes/public-build-tasks.js +5 -1
  60. package/dist/pwa/routes/public-utils.js +9 -12
  61. package/dist/pwa/routes/referral.js +5 -26
  62. package/dist/pwa/routes/rewards-apply.js +3 -2
  63. package/dist/pwa/routes/share-redirects.js +1 -1
  64. package/dist/pwa/routes/shareables-interactions.js +2 -1
  65. package/dist/pwa/routes/task-proposals.js +85 -9
  66. package/dist/pwa/routes/users-public.js +1 -4
  67. package/dist/pwa/routes/wallet-read.js +2 -14
  68. package/dist/pwa/routes/webauthn.js +1 -1
  69. package/dist/pwa/server.js +156 -469
  70. package/dist/settlement-math.js +3 -3
  71. package/dist/version.js +6 -4
  72. package/package.json +33 -7
  73. package/dist/index.js +0 -182
  74. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  75. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  76. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  77. package/dist/test-dispute.js +0 -153
  78. package/dist/test-manifest.js +0 -61
  79. package/dist/test-mcp-tools.js +0 -135
  80. package/dist/test-reputation.js +0 -116
  81. package/dist/test-skill-market.js +0 -101
@@ -674,6 +674,8 @@ async function render(page, params) {
674
674
  if (params[0] === 'notes') return renderMyNotes(app)
675
675
  if (params[0] === 'settings') return renderMyHome(app, 'settings')
676
676
  if (params[0] === 'advanced') return renderMyHome(app, 'advanced')
677
+ if (params[0] === 'quota-requests') return renderMyQuotaRequests(app)
678
+ if (params[0] === 'operator-claims') return renderMyOperatorClaims(app)
677
679
  return renderMyHome(app, 'dashboard')
678
680
  case 'note-new': return renderNoteCreate(app, params[0]) // order_id
679
681
  case 'u': return renderUserProfile(app, params[0])
@@ -713,6 +715,8 @@ async function render(page, params) {
713
715
  // 2026-05-24 #welcome 公开 ideas/邮箱订阅查看
714
716
  if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
715
717
  if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
718
+ if (params[0] === 'quota-requests') return renderAdminBuildTaskQuota(app)
719
+ if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app)
716
720
  if (params[0] === 'params') return renderAdminParams(app)
717
721
  if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
718
722
  if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
@@ -1285,14 +1289,17 @@ async function renderMyAdvanced(app) {
1285
1289
  app.innerHTML = shell(loading$(), 'me')
1286
1290
  const role = state.user.role
1287
1291
  const isTrusted = ['admin', 'verifier', 'logistics', 'arbitrator'].includes(role)
1288
- const [agentRes, skillsRes] = await Promise.all([
1292
+ const [agentRes, skillsRes, ocRes] = await Promise.all([
1289
1293
  GET('/agents/me/reputation').catch(() => null),
1290
1294
  GET('/skills/mine').catch(() => []),
1295
+ GET('/me/operator-claims').catch(() => null),
1291
1296
  ])
1292
1297
  const trustScore = Math.round(agentRes?.trust_score || 0)
1293
1298
  const level = agentRes?.level || 'new'
1294
1299
  const lvlColor = { legend: '#dc2626', quality: '#9333ea', trusted: '#4f46e5', new: '#9ca3af' }[level] || '#6b7280'
1295
1300
  const skillCount = (Array.isArray(skillsRes) ? skillsRes : []).length
1301
+ // 贡献归属入口:admin 常驻;普通用户仅当确有 operator-claim 关系(pending/active/history)时才显示,保持清爽
1302
+ const hasOperatorClaim = !!(ocRes && Array.isArray(ocRes.relationships) && ocRes.relationships.length)
1296
1303
 
1297
1304
  const card = (icon, label, sub, hash) => `
1298
1305
  <div class="card" onclick="location.hash='${hash}'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px">
@@ -1334,6 +1341,7 @@ async function renderMyAdvanced(app) {
1334
1341
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
1335
1342
  ${card('📜', t('Timeline'), t('全部事件按时间排列'), '#me/timeline')}
1336
1343
  ${card('📡', t('Webhook'), t('订阅事件 push 到外部端点'), '#me/webhooks')}
1344
+ ${(role === 'admin' || hasOperatorClaim) ? card('🪪', t('贡献归属'), t('待确认的 admin 关联 / 关联记录'), '#me/operator-claims') : ''}
1337
1345
  </div>
1338
1346
 
1339
1347
  <div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🧠 ${t('技能市场')}</div>
@@ -1756,7 +1764,7 @@ async function renderProfile(app) {
1756
1764
  </div>
1757
1765
  <div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
1758
1766
  <span style="color:#374151">${t('协议')}</span>
1759
- <a href="https://github.com/seasonsagents-art/webaz" target="_blank" style="color:#6366f1;text-decoration:none;font-size:12px">${t('源码仓库')} ↗</a>
1767
+ <a href="https://github.com/webaz-protocol/webaz" target="_blank" style="color:#6366f1;text-decoration:none;font-size:12px">${t('源码仓库')} ↗</a>
1760
1768
  </div>
1761
1769
  <div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
1762
1770
  <span style="color:#374151">🔔 ${t('推送通知')}</span>
@@ -3194,6 +3202,7 @@ async function renderAdminTaskProposals(app) {
3194
3202
  <div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
3195
3203
  <div style="font-weight:600;font-size:14px">${escHtml(p.title)}</div>${badge(p.status)}
3196
3204
  </div>
3205
+ <div style="font-family:monospace;font-size:11px;color:#6b7280;margin-top:3px">${T('案件 ID', 'Case ID')}: ${escHtml(p.case_id || p.id)}</div>
3197
3206
  <div style="font-size:13px;color:#52525B;line-height:1.5;margin-top:6px;white-space:pre-wrap">${escHtml(p.summary)}</div>
3198
3207
  ${field(T('建议领域', 'Area'), p.suggested_area)}
3199
3208
  ${field(T('预期结果', 'Outcome'), p.expected_outcome)}
@@ -3228,8 +3237,12 @@ async function renderAdminTaskProposals(app) {
3228
3237
  </div>
3229
3238
  ${field(T('风险', 'Risk'), d.risk_level)}${field(T('可自助认领', 'Auto-claimable'), d.auto_claimable === 1 || d.auto_claimable === true ? T('是', 'yes') : T('否(需真人)', 'no (human)'))}
3230
3239
  ${field(T('来源建议', 'Source proposal'), d.source_proposal_id)}${field(T('创建人', 'Created by'), d.created_by)}
3240
+ <div id="draft-preview-${escHtml(d.id)}" style="display:none;margin-top:8px;border-top:1px dashed #c7d2fe;padding-top:8px;font-size:12px;color:#52525B;line-height:1.5"></div>
3231
3241
  <div style="font-size:10px;color:#9ca3af;margin-top:6px">${T('发布前会校验交接字段;发布后进入正常任务板,可被参与者 agent 发现 / 认领 / 提交 PR。', 'Publish validates the handoff fields; once published it enters the normal task board — discoverable / claimable / PR-submittable by participant agents.')}</div>
3232
- <button onclick="publishDraft('${escHtml(d.id)}')" style="margin-top:8px;padding:6px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">${T('发布到任务板', 'Publish to board')}</button>
3242
+ <div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;align-items:center">
3243
+ <button onclick="previewDraft('${escHtml(d.id)}')" style="padding:6px 12px;border:1px solid #6366f1;background:#fff;color:#4338ca;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">👁 ${T('预览将发布内容', 'Preview what will be published')}</button>
3244
+ <button id="pub-btn-${escHtml(d.id)}" disabled title="${T('请先预览将发布的存储内容', 'Preview the stored content first')}" onclick="publishDraft('${escHtml(d.id)}')" style="padding:6px 14px;border:none;background:#d1d5db;color:#6b7280;border-radius:6px;font-size:12px;cursor:not-allowed;font-weight:600">${T('发布到任务板', 'Publish to board')}</button>
3245
+ </div>
3233
3246
  </div>`
3234
3247
  app.innerHTML = shell(`
3235
3248
  <div style="padding:14px;max-width:920px;margin:0 auto">
@@ -3300,10 +3313,41 @@ window.createTaskDraft = async (id) => {
3300
3313
  definition_of_done: v('dod'), expected_results: v('expect'),
3301
3314
  }
3302
3315
  const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/create-task-draft', body)
3316
+ if (r && r.error_code === 'RATE_LIMITED') { showRateLimitAffordance(r); return }
3303
3317
  if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Missing: ' : '缺少:') + r.missing.join(', ')) : (r.error || 'failed')); return }
3304
3318
  toast$(en ? 'Draft saved (unpublished)' : '草稿已保存(未发布)')
3305
3319
  renderAdminTaskProposals(document.getElementById('app'))
3306
3320
  }
3321
+ // Pre-publish preview: load the FULL stored draft body so publish is a decision against visible content.
3322
+ // Opening the preview also un-gates the (initially disabled) Publish button for this draft.
3323
+ window.previewDraft = async (taskId) => {
3324
+ // bilingual helper — T is local to renderAdminTaskProposals, NOT a global, so this top-level fn defines its own
3325
+ const T = (zh, en) => (window._lang === 'en' ? en : zh)
3326
+ const box = document.getElementById('draft-preview-' + taskId)
3327
+ if (!box) return
3328
+ box.style.display = ''
3329
+ box.innerHTML = t('加载中...')
3330
+ const r = await GET('/admin/build-task-drafts/' + encodeURIComponent(taskId)).catch(() => null)
3331
+ const d = r && r.draft
3332
+ if (!d) { box.innerHTML = `<span style="color:#dc2626">${T('预览加载失败', 'Preview failed to load')}</span>`; return }
3333
+ const m = d.agent_metadata || {}
3334
+ const li = (arr) => (Array.isArray(arr) && arr.length) ? `<ul style="margin:2px 0 6px 16px;padding:0">${arr.map((x) => `<li>${escHtml(String(x))}</li>`).join('')}</ul>` : `<div style="color:#9ca3af;margin-bottom:6px">—</div>`
3335
+ const txtBlock = (s) => `<div style="white-space:pre-wrap;margin-bottom:6px">${escHtml(String(s || '')) || '<span style="color:#9ca3af">—</span>'}</div>`
3336
+ const sec = (label, html) => `<div style="margin-top:6px"><div style="font-weight:600;color:#374151">${escHtml(label)}</div>${html}</div>`
3337
+ box.innerHTML = `<div style="font-size:11px;color:#6366f1;font-weight:600;margin-bottom:4px">${T('将要发布的存储内容(发布对此生效)', 'Stored content that will be published (publish acts on this)')}</div>`
3338
+ + sec(T('说明', 'Description'), txtBlock(d.description))
3339
+ + sec(T('验收标准', 'Acceptance criteria'), li(m.acceptance_criteria))
3340
+ + sec(T('验证命令', 'Verification commands'), li(m.verification_commands))
3341
+ + sec(T('允许路径', 'Allowed paths'), li(m.allowed_paths))
3342
+ + sec(T('禁止路径', 'Forbidden paths'), li(m.forbidden_paths))
3343
+ + sec(T('禁止动作', 'Forbidden actions'), li(m.prohibited_actions))
3344
+ + sec(T('交付物', 'Deliverables'), li(m.deliverables))
3345
+ + sec(T('完成定义', 'Definition of done'), txtBlock(m.definition_of_done))
3346
+ + sec(T('预期结果', 'Expected results'), txtBlock(m.expected_results))
3347
+ const btn = document.getElementById('pub-btn-' + taskId)
3348
+ if (btn) { btn.disabled = false; btn.title = ''; btn.style.cssText = 'padding:6px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600' }
3349
+ }
3350
+
3307
3351
  window.publishDraft = async (taskId) => {
3308
3352
  const en = window._lang === 'en'
3309
3353
  const r = await POST('/admin/build-task-drafts/' + encodeURIComponent(taskId) + '/publish', {})
@@ -3312,6 +3356,449 @@ window.publishDraft = async (taskId) => {
3312
3356
  renderAdminTaskProposals(document.getElementById('app'))
3313
3357
  }
3314
3358
 
3359
+ // ── PR #18 build-task quota-increase requests ─────────────────────────────────
3360
+ const _qT = (zh, en) => (window._lang === 'en' ? en : zh)
3361
+ const _qStatusBadge = (s) => {
3362
+ const map = {
3363
+ pending: ['#fef9c3', '#854d0e', _qT('待审核', 'Pending')],
3364
+ approved: ['#dcfce7', '#166534', _qT('已批准', 'Approved')],
3365
+ rejected: ['#fee2e2', '#991b1b', _qT('已拒绝', 'Rejected')],
3366
+ expired: ['#f3f4f6', '#6b7280', _qT('已过期', 'Expired')],
3367
+ exhausted: ['#e0e7ff', '#3730a3', _qT('已用完', 'Exhausted')],
3368
+ revoked: ['#fae8ff', '#86198f', _qT('已撤销', 'Revoked')],
3369
+ }
3370
+ const [bg, fg, label] = map[s] || ['#f3f4f6', '#6b7280', s]
3371
+ return `<span style="font-size:11px;background:${bg};color:${fg};padding:2px 8px;border-radius:99px;font-weight:600">${escHtml(label)}</span>`
3372
+ }
3373
+
3374
+ // RATE_LIMITED affordance — shown when build-task creation is capped (structured 429 response).
3375
+ window.showRateLimitAffordance = (r) => {
3376
+ const limit = (r && r.limit) != null ? r.limit : '?'
3377
+ const used = (r && r.used) != null ? r.used : '?'
3378
+ document.getElementById('quota-rl-overlay')?.remove()
3379
+ const ov = document.createElement('div')
3380
+ ov.id = 'quota-rl-overlay'
3381
+ ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px'
3382
+ ov.innerHTML = `
3383
+ <div style="background:#fff;border-radius:12px;max-width:420px;width:100%;padding:20px;box-shadow:0 10px 40px rgba(0,0,0,.2)">
3384
+ <div style="font-size:16px;font-weight:700;color:#991b1b;margin-bottom:8px">⚠️ ${_qT('已达每日建任务上限', 'Daily task-creation limit reached')}</div>
3385
+ <div style="font-size:13px;color:#374151;line-height:1.6;margin-bottom:14px">
3386
+ ${_qT('当前上限', 'Current limit')}: <b>${escHtml(String(limit))}</b> ${_qT('个 / 24 小时', 'tasks / 24h')} · ${_qT('已用', 'Used')}: <b>${escHtml(String(used))}</b><br>
3387
+ ${_qT('需要更多额度需经根管理员批准。', 'More headroom requires root-admin approval.')}
3388
+ </div>
3389
+ <div style="display:flex;gap:8px;justify-content:flex-end">
3390
+ <button onclick="document.getElementById('quota-rl-overlay').remove()" style="padding:8px 14px;border:1px solid #d1d5db;background:#fff;color:#374151;border-radius:8px;font-size:13px;cursor:pointer">${_qT('关闭', 'Close')}</button>
3391
+ <button onclick="document.getElementById('quota-rl-overlay').remove();navigate('#me/quota-requests')" style="padding:8px 14px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('申请增加额度', 'Request extra quota')}</button>
3392
+ </div>
3393
+ </div>`
3394
+ document.body.appendChild(ov)
3395
+ }
3396
+
3397
+ // Requester view — own quota requests + a new-request form.
3398
+ async function renderMyQuotaRequests(app) {
3399
+ if (!state.user) { renderLogin(); return }
3400
+ app.innerHTML = shell(loading$(), 'me')
3401
+ const r = await GET('/me/quota-requests').catch(() => null)
3402
+ if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'me'); return }
3403
+ const reqs = r.requests || []
3404
+ const hasPending = reqs.some(x => x.status === 'pending')
3405
+ const field = (label, html) => `<div style="margin-bottom:10px"><label style="display:block;font-size:12px;color:#6b7280;margin-bottom:4px">${escHtml(label)}</label>${html}</div>`
3406
+ const inputStyle = 'width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;box-sizing:border-box'
3407
+
3408
+ const form = hasPending
3409
+ ? `<div class="card" style="padding:14px;margin-bottom:14px;background:#fffbeb;border:1px solid #fde68a">
3410
+ <div style="font-size:13px;color:#854d0e">${_qT('你已有一个待审核的申请 — 每种额度类型同时只能有一个待审核申请。', 'You already have a pending request — only one pending request per quota type is allowed.')}</div>
3411
+ </div>`
3412
+ : `<div class="card" style="padding:16px;margin-bottom:16px">
3413
+ <div style="font-size:14px;font-weight:700;margin-bottom:12px">📝 ${_qT('申请增加建任务额度', 'Request extra build-task quota')}</div>
3414
+ ${field(_qT('额外任务数(必填,正整数)', 'Extra tasks (required, positive integer)'), `<input id="q-count" type="number" min="1" placeholder="10" style="${inputStyle}">`)}
3415
+ ${field(_qT('理由(必填)', 'Reason (required)'), `<textarea id="q-reason" rows="3" placeholder="${_qT('为什么需要更多额度', 'Why you need more quota')}" style="${inputStyle}"></textarea>`)}
3416
+ ${field(_qT('关联任务/提案/PR(每行一个,可选)', 'Linked task/proposal/PR refs (one per line, optional)'), `<textarea id="q-refs" rows="2" placeholder="#17\\ntp_..." style="${inputStyle}"></textarea>`)}
3417
+ ${field(_qT('紧急程度', 'Urgency'), `<select id="q-urgency" style="${inputStyle}"><option value="normal">${_qT('普通', 'Normal')}</option><option value="low">${_qT('低', 'Low')}</option><option value="high">${_qT('高', 'High')}</option></select>`)}
3418
+ ${field(_qT('期望有效期(小时,可选)', 'Requested duration (hours, optional)'), `<input id="q-duration" type="number" min="1" placeholder="72" style="${inputStyle}">`)}
3419
+ <button onclick="submitQuotaRequest()" style="margin-top:6px;padding:9px 16px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('提交申请', 'Submit request')}</button>
3420
+ </div>`
3421
+
3422
+ const card = (x) => {
3423
+ const granted = x.granted_count != null ? x.granted_count : null
3424
+ const remaining = x.remaining != null ? x.remaining : null
3425
+ return `<div class="card" style="padding:14px;margin-bottom:10px">
3426
+ <div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
3427
+ <div style="font-size:13px;font-weight:600">${escHtml(_qT('额外', 'Extra'))} ${escHtml(String(x.requested_extra_count))} · ${escHtml(x.urgency || 'normal')}</div>
3428
+ ${_qStatusBadge(x.status)}
3429
+ </div>
3430
+ <div style="font-size:12px;color:#374151;margin-top:6px;white-space:pre-wrap">${escHtml(x.reason || '')}</div>
3431
+ ${(x.linked_refs && x.linked_refs.length) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('关联', 'Refs')}: ${x.linked_refs.map(escHtml).join(', ')}</div>` : ''}
3432
+ ${x.status === 'approved' ? `<div style="font-size:12px;color:#166534;margin-top:6px">${_qT('授权', 'Granted')}: ${escHtml(String(granted))} · ${_qT('剩余', 'Remaining')}: <b>${escHtml(String(remaining))}</b>${x.expires_at ? ` · ${_qT('到期', 'Expires')}: ${escHtml(x.expires_at)}` : ''}</div>` : ''}
3433
+ ${x.status === 'exhausted' ? `<div style="font-size:12px;color:#3730a3;margin-top:6px">${_qT('授权已用完', 'Grant fully used')} (${escHtml(String(granted))})</div>` : ''}
3434
+ ${x.status === 'rejected' && x.decision_note ? `<div style="font-size:12px;color:#991b1b;margin-top:6px">${_qT('拒绝原因', 'Rejection reason')}: ${escHtml(x.decision_note)}</div>` : ''}
3435
+ <div style="font-size:10px;color:#9ca3af;margin-top:6px">${escHtml(x.created_at || '')}</div>
3436
+ </div>`
3437
+ }
3438
+
3439
+ const body = `
3440
+ <div style="max-width:560px;margin:0 auto">
3441
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
3442
+ <div style="font-size:18px;font-weight:700">🎟️ ${_qT('我的额度申请', 'My quota requests')}</div>
3443
+ <a href="#me" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
3444
+ </div>
3445
+ <div class="card" style="padding:12px;margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#fff)">
3446
+ <div style="font-size:12px;color:#6b7280">${_qT('当前可用临时额度', 'Current temporary quota available')}</div>
3447
+ <div style="font-size:22px;font-weight:700;color:#4338ca">${escHtml(String(r.remaining_quota || 0))}</div>
3448
+ </div>
3449
+ ${form}
3450
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">${_qT('历史申请', 'Request history')}</div>
3451
+ ${reqs.length ? reqs.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无申请', 'No requests yet')}</div>`}
3452
+ </div>`
3453
+ app.innerHTML = shell(body, 'me')
3454
+ }
3455
+
3456
+ window.submitQuotaRequest = async () => {
3457
+ const v = (id) => (document.getElementById(id)?.value || '').trim()
3458
+ const count = Number(v('q-count'))
3459
+ const reason = v('q-reason')
3460
+ if (!count || count <= 0) { toast$(_qT('请填写正整数额外任务数', 'Enter a positive extra-task count')); return }
3461
+ if (reason.length < 5) { toast$(_qT('请填写理由(至少 5 字)', 'Reason required (>= 5 chars)')); return }
3462
+ const refs = v('q-refs').split('\n').map(s => s.trim()).filter(Boolean)
3463
+ const duration = v('q-duration')
3464
+ const body = { requested_extra_count: count, reason, linked_refs: refs, urgency: v('q-urgency') || 'normal' }
3465
+ if (duration) body.requested_duration_hours = Number(duration)
3466
+ const r = await POST('/me/quota-requests', body)
3467
+ if (r && r.error) { toast$(r.error_code === 'ALREADY_PENDING' ? _qT('你已有一个待审核申请', 'You already have a pending request') : (r.error || _qT('提交失败', 'Submit failed'))); return }
3468
+ toast$(_qT('申请已提交,等待根管理员审核', 'Submitted — awaiting root-admin review'))
3469
+ renderMyQuotaRequests(document.getElementById('app'))
3470
+ }
3471
+
3472
+ // ── Admin operator-claim workflow (Phase 2): link an admin SEAT → a personal contributor account ──
3473
+ function _ocStatusBadge(s) {
3474
+ const map = {
3475
+ proposed: ['#fef9c3', '#854d0e', _qT('待贡献人确认', 'Awaiting contributor')],
3476
+ confirmed: ['#dbeafe', '#1e40af', _qT('待 root 审批', 'Awaiting root approval')],
3477
+ rejected_by_contributor: ['#fee2e2', '#991b1b', _qT('贡献人已拒绝', 'Rejected by contributor')],
3478
+ approved: ['#dcfce7', '#166534', _qT('已生效', 'Active')],
3479
+ rejected_by_root: ['#fee2e2', '#991b1b', _qT('root 已拒绝', 'Rejected by root')],
3480
+ revoked: ['#fae8ff', '#86198f', _qT('已撤销', 'Revoked')],
3481
+ superseded: ['#f3f4f6', '#6b7280', _qT('已被取代', 'Superseded')],
3482
+ }
3483
+ const [bg, fg, label] = map[s] || ['#f3f4f6', '#6b7280', s]
3484
+ return `<span style="background:${bg};color:${fg};padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600">${escHtml(label)}</span>`
3485
+ }
3486
+
3487
+ // Page for any user: (a) if admin — their seat + a "link personal contributor account" form;
3488
+ // (b) for everyone — claims pointing at ME awaiting my accept/reject.
3489
+ async function renderMyOperatorClaims(app) {
3490
+ if (!state.user) { renderLogin(); return }
3491
+ app.innerHTML = shell(loading$(), 'me')
3492
+ const isAdmin = state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin'))
3493
+ const pend = await GET('/me/operator-claim-confirmations').catch(() => null)
3494
+ const rel = await GET('/me/operator-claims').catch(() => null) // ALL relationships pointing at me (active/history)
3495
+ const mine = isAdmin ? await GET('/admin/operator-claims/me').catch(() => null) : null
3496
+ const inputStyle = 'width:100%;padding:8px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;box-sizing:border-box'
3497
+ const field = (label, html) => `<div style="margin-bottom:10px"><label style="display:block;font-size:12px;color:#6b7280;margin-bottom:4px">${escHtml(label)}</label>${html}</div>`
3498
+
3499
+ // active approved claim → either an "申请解除" button or a pending-review note. Either PARTY may
3500
+ // request unlink (admin-seat owner OR contributor), so both claimRow and relCard reuse this.
3501
+ const unlinkAreaFor = (c) => {
3502
+ const active = c.status === 'approved' && c.approved
3503
+ if (!active) return ''
3504
+ return c.unlink_pending
3505
+ ? `<div style="font-size:11px;color:#b45309;margin-top:8px">⏳ ${_qT('解除申请审批中(待 root)', 'Unlink request pending root review')}</div>`
3506
+ : `<button onclick="requestUnlinkOperatorClaim('${escHtml(c.approved.event_id)}')" style="margin-top:8px;padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#b91c1c;border-radius:8px;font-size:12px;cursor:pointer">${_qT('申请解除', 'Request unlink')}</button>`
3507
+ }
3508
+
3509
+ const claimRow = (c) => `<div class="card" style="padding:12px;margin-bottom:8px">
3510
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
3511
+ <div style="font-size:12px;font-weight:600">→ ${escHtml(c.contributor_account_id)}</div>${_ocStatusBadge(c.status)}
3512
+ </div>
3513
+ <div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(c.proposed_at || '')} · ${escHtml(c.claimed_event_id)}</div>${unlinkAreaFor(c)}
3514
+ </div>`
3515
+
3516
+ const adminBlock = isAdmin ? `
3517
+ <div class="card" style="padding:16px;margin-bottom:16px">
3518
+ <div style="font-size:14px;font-weight:700;margin-bottom:4px">🔗 ${_qT('关联个人贡献账号', 'Link a personal contributor account')}</div>
3519
+ <div style="font-size:12px;color:#6b7280;margin-bottom:12px">${_qT('把这个管理席位的协调贡献,归属到你的真实个人账号。需对方确认 + 根管理员审批。', 'Attribute this admin seat\'s coordination work to your real personal account. Requires the contributor to accept + root approval.')}</div>
3520
+ ${field(_qT('贡献人账号 ID(必填)', 'Contributor account ID (required)'), `<input id="oc-contributor" placeholder="usr_..." style="${inputStyle}">`)}
3521
+ ${field(_qT('理由(可选)', 'Rationale (optional)'), `<input id="oc-rationale" placeholder="${_qT('为何关联', 'why')}" style="${inputStyle}">`)}
3522
+ <button onclick="submitOperatorClaim()" style="margin-top:4px;padding:9px 16px;border:none;background:#4338ca;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('发起关联', 'Propose link')}</button>
3523
+ <div style="font-size:13px;font-weight:600;margin:16px 0 8px">${_qT('本席位的关联记录', 'This seat\'s claims')}</div>
3524
+ ${(mine && mine.claims && mine.claims.length) ? mine.claims.map(claimRow).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无', 'None yet')}</div>`}
3525
+ </div>` : '' /* adminBlock */
3526
+
3527
+ const pendList = (pend && pend.pending) || []
3528
+ const confirmCard = (c) => `<div class="card" style="padding:14px;margin-bottom:10px;background:#fffbeb;border:1px solid #fde68a">
3529
+ <div style="font-size:13px">${_qT('管理席位', 'Admin seat')} <b>${escHtml(c.admin_account_id)}</b> ${_qT('请求关联到你的账号作为贡献归属。', 'requests to attribute its coordination work to your account.')}</div>
3530
+ <div style="font-size:10px;color:#9ca3af;margin:6px 0">${escHtml(c.claimed_event_id)}</div>
3531
+ <div style="display:flex;gap:8px">
3532
+ <button onclick="confirmOperatorClaim('${escHtml(c.claimed_event_id)}','accepted')" style="padding:8px 14px;border:none;background:#166534;color:#fff;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">${_qT('接受', 'Accept')}</button>
3533
+ <button onclick="confirmOperatorClaim('${escHtml(c.claimed_event_id)}','rejected')" style="padding:8px 14px;border:1px solid #d1d5db;background:#fff;color:#991b1b;border-radius:8px;font-size:13px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
3534
+ </div>
3535
+ </div>`
3536
+
3537
+ // 我的贡献归属关系(已生效/历史)+ approved 关系可「申请解除」(不是直接撤销;需 Passkey + root 审批)
3538
+ const relList = (rel && rel.relationships) || []
3539
+ const relCard = (c) => {
3540
+ return `<div class="card" style="padding:12px;margin-bottom:8px">
3541
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
3542
+ <div style="font-size:12px;font-weight:600">${escHtml(c.admin_account_id)} → ${escHtml(c.contributor_account_id)}</div>${_ocStatusBadge(c.status)}
3543
+ </div>
3544
+ <div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(c.proposed_at || '')}</div>${unlinkAreaFor(c)}
3545
+ </div>`
3546
+ }
3547
+
3548
+ const body = `<div style="max-width:560px;margin:0 auto">
3549
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
3550
+ <div style="font-size:18px;font-weight:700">🪪 ${_qT('贡献归属', 'Contribution attribution')}</div>
3551
+ <a href="#me" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
3552
+ </div>
3553
+ ${adminBlock}
3554
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">${_qT('待我确认的关联', 'Awaiting my confirmation')}</div>
3555
+ ${pendList.length ? pendList.map(confirmCard).join('') : `<div style="font-size:13px;color:#9ca3af;margin-bottom:8px">${_qT('没有待确认的关联', 'No pending links')}</div>`}
3556
+ <div style="font-size:13px;font-weight:600;margin:16px 0 8px">${_qT('我的贡献归属关系 / 历史', 'My contribution-attribution relationships / history')}</div>
3557
+ ${relList.length ? relList.map(relCard).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无关系', 'No relationships yet')}</div>`}
3558
+ </div>`
3559
+ app.innerHTML = shell(body, 'me')
3560
+ }
3561
+
3562
+ window.requestUnlinkOperatorClaim = async (approvedEventId) => {
3563
+ if (!confirm(_qT('确认申请解除该贡献归属关系?需 Passkey 验证,且最终由 root 审批。', 'Request to unlink this attribution relationship? Requires Passkey, then root approval.'))) return
3564
+ let token
3565
+ try { token = await requestPasskeyGate('operator_claim_unlink', { approved_event_id: approvedEventId }) }
3566
+ catch (e) { toast$(e.message || _qT('Passkey 验证失败', 'Passkey verification failed')); return }
3567
+ const reason = (prompt(_qT('解除理由(可选)', 'Reason (optional)')) || '').trim() || undefined
3568
+ const r = await POST('/me/operator-claims/' + encodeURIComponent(approvedEventId) + '/request-unlink', { webauthn_token: token, reason })
3569
+ if (r && r.error) { toast$(r.message || r.error || _qT('提交失败', 'Failed')); return }
3570
+ toast$(_qT('解除申请已提交,等待 root 审批', 'Unlink request submitted — awaiting root review'))
3571
+ renderMyOperatorClaims(document.getElementById('app'))
3572
+ }
3573
+
3574
+ window.submitOperatorClaim = async () => {
3575
+ const contributor = (document.getElementById('oc-contributor')?.value || '').trim()
3576
+ const rationale = (document.getElementById('oc-rationale')?.value || '').trim()
3577
+ if (!contributor) { toast$(_qT('请填写贡献人账号 ID', 'Enter contributor account ID')); return }
3578
+ const r = await POST('/admin/operator-claims', { contributor_account_id: contributor, rationale })
3579
+ if (r && r.error) { toast$(r.message || r.error || _qT('发起失败', 'Failed')); return }
3580
+ toast$(_qT('已发起,等待对方确认 + root 审批', 'Proposed — awaiting contributor + root'))
3581
+ renderMyOperatorClaims(document.getElementById('app'))
3582
+ }
3583
+ window.confirmOperatorClaim = async (claimedEventId, decision) => {
3584
+ const r = await POST('/me/operator-claim-confirmations/' + encodeURIComponent(claimedEventId), { decision })
3585
+ if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
3586
+ toast$(decision === 'accepted' ? _qT('已接受', 'Accepted') : _qT('已拒绝', 'Rejected'))
3587
+ renderMyOperatorClaims(document.getElementById('app'))
3588
+ }
3589
+
3590
+ // ROOT review queue for operator claims.
3591
+ async function renderAdminOperatorClaims(app, statusFilter) {
3592
+ if (!state.user) { renderLogin(); return }
3593
+ const isRoot = (state.user.admin_type || 'root') === 'root' && (state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin')))
3594
+ if (!isRoot) { app.innerHTML = shell(`<div class="alert alert-danger">${_qT('仅限根管理员', 'Root admin only')}</div>`, 'admin'); return }
3595
+ app.innerHTML = shell(loading$(), 'admin')
3596
+ const sf = statusFilter || 'confirmed'
3597
+ const r = await GET('/admin/operator-claims' + (sf === 'all' ? '' : '?status=' + encodeURIComponent(sf))).catch(() => null)
3598
+ if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'admin'); return }
3599
+ const claims = r.claims || []
3600
+ const unlinkRes = await GET('/admin/operator-claims/unlink/requests').catch(() => null)
3601
+ const unlinkReqs = (unlinkRes && unlinkRes.requests) || []
3602
+ const inputStyle = 'width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;box-sizing:border-box'
3603
+ const filterBtn = (s, label) => `<button onclick="renderAdminOperatorClaims(document.getElementById('app'),'${s}')" style="padding:5px 10px;border:1px solid ${sf === s ? '#4338ca' : '#d1d5db'};background:${sf === s ? '#4338ca' : '#fff'};color:${sf === s ? '#fff' : '#374151'};border-radius:6px;font-size:12px;cursor:pointer">${escHtml(label)}</button>`
3604
+ const unlinkCard = (u) => {
3605
+ const rid = u.request_event_id
3606
+ // When THIS root is a party to the relationship/request (self-or-related), root may still decide it
3607
+ // but MUST mark the conflict honestly: approval_kind ∈ {root_approval, founder_bootstrap_override}
3608
+ // (never independent_governance) + conflict_disclosure = self_or_related. Mirrors approveClaim.
3609
+ const markingForm = u.self_or_related ? `
3610
+ <div style="margin-top:8px;padding-top:8px;border-top:1px dashed #fed7aa">
3611
+ <div style="font-size:11px;color:#b45309;margin-bottom:6px">⚠️ ${_qT('你是该关系/申请的关联方:必须如实标记(不可 independent_governance)', 'You are a party to this relationship/request: mark honestly (independent_governance not allowed)')}</div>
3612
+ <div style="display:flex;gap:6px">
3613
+ <select id="uak-${rid}" style="${inputStyle}">
3614
+ <option value="root_approval">root_approval</option>
3615
+ <option value="founder_bootstrap_override">founder_bootstrap_override</option>
3616
+ </select>
3617
+ <select id="ucd-${rid}" style="${inputStyle}">
3618
+ <option value="self_or_related" selected>self_or_related</option>
3619
+ </select>
3620
+ </div>
3621
+ </div>` : ''
3622
+ return `<div class="card" style="padding:12px;margin-bottom:8px;background:#fff7ed;border:1px solid #fed7aa">
3623
+ <div style="font-size:12px;font-weight:600">🔓 ${escHtml(u.admin_account_id)} → ${escHtml(u.contributor_account_id)}${u.self_or_related ? ' 🪞' : ''}</div>
3624
+ <div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('申请人', 'Requested by')}: ${escHtml(u.requested_by)} (${escHtml(u.requester_role)})${u.reason ? ' · ' + escHtml(u.reason) : ''}</div>
3625
+ ${markingForm}
3626
+ <div style="display:flex;gap:8px;margin-top:8px">
3627
+ <button onclick="approveUnlinkReq('${escHtml(rid)}')" style="padding:6px 12px;border:none;background:#b91c1c;color:#fff;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准解除', 'Approve unlink')}</button>
3628
+ <button onclick="rejectUnlinkReq('${escHtml(rid)}')" style="padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#374151;border-radius:8px;font-size:12px;cursor:pointer">${_qT('驳回', 'Reject')}</button>
3629
+ </div>
3630
+ </div>`
3631
+ }
3632
+
3633
+ const card = (c) => {
3634
+ const selfLink = c.admin_account_id === c.contributor_account_id
3635
+ const id = c.claimed_event_id
3636
+ const approveForm = (c.status === 'confirmed' || (selfLink && c.status === 'proposed')) ? `
3637
+ <div style="margin-top:10px;padding-top:10px;border-top:1px solid #eee">
3638
+ ${selfLink ? `<div style="font-size:11px;color:#b45309;margin-bottom:6px">⚠️ ${_qT('自链(席位=贡献人):必须 founder_bootstrap_override + self_or_related', 'Self-link (seat == contributor): must be founder_bootstrap_override + self_or_related')}</div>` : ''}
3639
+ <div style="display:flex;gap:6px;margin-bottom:6px">
3640
+ <select id="ak-${id}" style="${inputStyle}">
3641
+ ${selfLink ? '' : `<option value="independent_governance">independent_governance</option>`}
3642
+ <option value="root_approval">root_approval</option>
3643
+ <option value="founder_bootstrap_override">founder_bootstrap_override</option>
3644
+ </select>
3645
+ <select id="cd-${id}" style="${inputStyle}">
3646
+ <option value="none">none</option>
3647
+ <option value="self_or_related"${selfLink ? ' selected' : ''}>self_or_related</option>
3648
+ </select>
3649
+ </div>
3650
+ <div style="display:flex;gap:8px">
3651
+ <button onclick="approveOperatorClaim('${escHtml(id)}')" style="padding:7px 14px;border:none;background:#166534;color:#fff;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准', 'Approve')}</button>
3652
+ <button onclick="rejectOperatorClaim('${escHtml(id)}')" style="padding:7px 14px;border:1px solid #d1d5db;background:#fff;color:#991b1b;border-radius:8px;font-size:12px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
3653
+ </div>
3654
+ </div>` : ''
3655
+ const revokeBtn = (c.status === 'approved' && c.approved) ? `<button onclick="revokeOperatorClaim('${escHtml(c.approved.event_id)}')" style="margin-top:8px;padding:6px 12px;border:1px solid #d1d5db;background:#fff;color:#86198f;border-radius:8px;font-size:12px;cursor:pointer">${_qT('撤销', 'Revoke')}</button>` : ''
3656
+ return `<div class="card" style="padding:14px;margin-bottom:10px">
3657
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
3658
+ <div style="font-size:12px;font-weight:600">${escHtml(c.admin_account_id)} → ${escHtml(c.contributor_account_id)}${selfLink ? ' 🪞' : ''}</div>${_ocStatusBadge(c.status)}
3659
+ </div>
3660
+ <div style="font-size:11px;color:#6b7280;margin-top:4px">${c.confirmation ? `${_qT('贡献人', 'Contributor')}: ${escHtml(c.confirmation.decision)}` : _qT('未确认', 'not confirmed')} · ${escHtml(c.proposed_at || '')}</div>
3661
+ ${approveForm}${revokeBtn}
3662
+ </div>`
3663
+ }
3664
+
3665
+ const body = `<div style="max-width:620px;margin:0 auto">
3666
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
3667
+ <div style="font-size:18px;font-weight:700">🪪 ${_qT('操作席位关联审批', 'Operator-claim review')}</div>
3668
+ <a href="#admin/protocol" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
3669
+ </div>
3670
+ ${unlinkReqs.length ? `<div style="font-size:13px;font-weight:700;color:#b91c1c;margin-bottom:8px">🔓 ${_qT('待审批的解除申请', 'Pending unlink requests')} (${unlinkReqs.length})</div>${unlinkReqs.map(unlinkCard).join('')}<div style="height:14px"></div>` : ''}
3671
+ <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px">
3672
+ ${filterBtn('confirmed', _qT('待审批', 'Awaiting approval'))}${filterBtn('proposed', _qT('待确认', 'Proposed'))}${filterBtn('approved', _qT('已生效', 'Active'))}${filterBtn('all', _qT('全部', 'All'))}
3673
+ </div>
3674
+ ${claims.length ? claims.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无', 'None')}</div>`}
3675
+ </div>`
3676
+ app.innerHTML = shell(body, 'admin')
3677
+ }
3678
+
3679
+ window.approveOperatorClaim = async (id) => {
3680
+ const ak = document.getElementById('ak-' + id)?.value
3681
+ const cd = document.getElementById('cd-' + id)?.value
3682
+ const r = await POST('/admin/operator-claims/' + encodeURIComponent(id) + '/approve', { approval_kind: ak, conflict_disclosure: cd })
3683
+ if (r && r.error) { toast$(r.message || r.error || _qT('审批失败', 'Approve failed')); return }
3684
+ toast$(_qT('已批准', 'Approved')); renderAdminOperatorClaims(document.getElementById('app'))
3685
+ }
3686
+ window.rejectOperatorClaim = async (id) => {
3687
+ const r = await POST('/admin/operator-claims/' + encodeURIComponent(id) + '/reject', {})
3688
+ if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
3689
+ toast$(_qT('已拒绝', 'Rejected')); renderAdminOperatorClaims(document.getElementById('app'))
3690
+ }
3691
+ window.revokeOperatorClaim = async (approvedId) => {
3692
+ const r = await POST('/admin/operator-claims/' + encodeURIComponent(approvedId) + '/revoke', {})
3693
+ if (r && r.error) { toast$(r.message || r.error || _qT('撤销失败', 'Revoke failed')); return }
3694
+ toast$(_qT('已撤销', 'Revoked')); renderAdminOperatorClaims(document.getElementById('app'))
3695
+ }
3696
+ window.approveUnlinkReq = async (requestId) => {
3697
+ if (!confirm(_qT('批准后将解除该贡献归属关系,确认?', 'Approving will unlink (revoke) this attribution. Confirm?'))) return
3698
+ // marking selectors only render when root is self-or-related; pass them through when present.
3699
+ const ak = document.getElementById('uak-' + requestId)?.value
3700
+ const cd = document.getElementById('ucd-' + requestId)?.value
3701
+ const body = ak ? { approval_kind: ak, conflict_disclosure: cd } : {}
3702
+ const r = await POST('/admin/operator-claims/unlink/' + encodeURIComponent(requestId) + '/approve', body)
3703
+ if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
3704
+ toast$(_qT('已批准解除', 'Unlink approved')); renderAdminOperatorClaims(document.getElementById('app'))
3705
+ }
3706
+ window.rejectUnlinkReq = async (requestId) => {
3707
+ const ak = document.getElementById('uak-' + requestId)?.value
3708
+ const cd = document.getElementById('ucd-' + requestId)?.value
3709
+ const body = ak ? { approval_kind: ak, conflict_disclosure: cd } : {}
3710
+ const r = await POST('/admin/operator-claims/unlink/' + encodeURIComponent(requestId) + '/reject', body)
3711
+ if (r && r.error) { toast$(r.message || r.error || _qT('操作失败', 'Failed')); return }
3712
+ toast$(_qT('已驳回,关系仍有效', 'Rejected — relationship stays active')); renderAdminOperatorClaims(document.getElementById('app'))
3713
+ }
3714
+
3715
+ // ROOT admin review page.
3716
+ async function renderAdminBuildTaskQuota(app, statusFilter) {
3717
+ if (!state.user) { renderLogin(); return }
3718
+ const isRoot = (state.user.admin_type || 'root') === 'root' && (state.user.role === 'admin' || (Array.isArray(state.user.roles) && state.user.roles.includes('admin')))
3719
+ if (!isRoot) { app.innerHTML = shell(`<div class="alert alert-danger">${_qT('仅限根管理员', 'Root admin only')}</div>`, 'admin'); return }
3720
+ app.innerHTML = shell(loading$(), 'admin')
3721
+ const sf = statusFilter || 'pending'
3722
+ const r = await GET('/admin/quota-requests' + (sf === 'all' ? '' : '?status=' + encodeURIComponent(sf))).catch(() => null)
3723
+ if (!r || r.error) { app.innerHTML = shell(alert$('error', (r && r.error) || _qT('加载失败', 'Failed to load')), 'admin'); return }
3724
+ const reqs = r.requests || []
3725
+ const inputStyle = 'width:100%;padding:7px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;box-sizing:border-box'
3726
+ const filterBtn = (s, label) => `<button onclick="renderAdminBuildTaskQuota(document.getElementById('app'),'${s}')" style="padding:5px 10px;border:1px solid ${sf === s ? '#4338ca' : '#d1d5db'};background:${sf === s ? '#4338ca' : '#fff'};color:${sf === s ? '#fff' : '#374151'};border-radius:6px;font-size:12px;cursor:pointer">${escHtml(label)}</button>`
3727
+
3728
+ const card = (x) => {
3729
+ const pending = x.status === 'pending'
3730
+ return `<div class="card" style="padding:14px;margin-bottom:10px">
3731
+ <div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
3732
+ <div style="font-size:13px;font-weight:600">${escHtml(x.requester_user_id)} · ${_qT('请求', 'Wants')} <b>${escHtml(String(x.requested_extra_count))}</b> · ${escHtml(x.urgency || 'normal')}</div>
3733
+ ${_qStatusBadge(x.status)}
3734
+ </div>
3735
+ <div style="font-size:12px;color:#374151;margin-top:6px;white-space:pre-wrap">${escHtml(x.reason || '')}</div>
3736
+ ${(x.linked_refs && x.linked_refs.length) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('关联', 'Refs')}: ${x.linked_refs.map(escHtml).join(', ')}</div>` : ''}
3737
+ <div style="font-size:10px;color:#9ca3af;margin-top:4px">${escHtml(x.created_at || '')} · ${escHtml(x.id)}</div>
3738
+ <div id="usage-${escHtml(x.id)}" style="font-size:11px;color:#6b7280;margin-top:4px"><button onclick="loadQuotaUsage('${escHtml(x.id)}')" style="padding:3px 8px;border:1px solid #d1d5db;background:#fff;border-radius:6px;font-size:11px;cursor:pointer">${_qT('查看申请人近 24h 用量', 'Load requester 24h usage')}</button></div>
3739
+ ${x.status === 'approved' ? `<div style="font-size:12px;color:#166534;margin-top:6px">${_qT('授权', 'Granted')}: ${escHtml(String(x.granted_count))} · ${_qT('剩余', 'Remaining')}: ${escHtml(String(x.remaining))}${x.expires_at ? ` · ${_qT('到期', 'Expires')}: ${escHtml(x.expires_at)}` : ''}
3740
+ <button onclick="revokeQuotaReq('${escHtml(x.id)}')" style="margin-left:8px;padding:3px 8px;border:1px solid #c026d3;background:#fff;color:#86198f;border-radius:6px;font-size:11px;cursor:pointer">${_qT('撤销', 'Revoke')}</button></div>` : ''}
3741
+ ${(x.decision_note && (x.status === 'rejected' || x.status === 'approved' || x.status === 'revoked')) ? `<div style="font-size:11px;color:#6b7280;margin-top:4px">${_qT('备注', 'Note')}: ${escHtml(x.decision_note)}</div>` : ''}
3742
+ ${pending ? `
3743
+ <div style="margin-top:10px;border-top:1px solid #f1f1f4;padding-top:10px;display:grid;gap:8px">
3744
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
3745
+ <input id="ap-count-${escHtml(x.id)}" type="number" min="1" value="${escHtml(String(x.requested_extra_count))}" placeholder="${_qT('授权数', 'Grant count')}" style="${inputStyle}">
3746
+ <input id="ap-dur-${escHtml(x.id)}" type="number" min="1" value="${escHtml(String(x.requested_duration_hours || 72))}" placeholder="${_qT('有效期(小时)', 'Duration (h)')}" style="${inputStyle}">
3747
+ </div>
3748
+ <input id="ap-note-${escHtml(x.id)}" placeholder="${_qT('批准备注(可选)', 'Approval note (optional)')}" style="${inputStyle}">
3749
+ <input id="rj-note-${escHtml(x.id)}" placeholder="${_qT('拒绝原因(可选)', 'Rejection note (optional)')}" style="${inputStyle}">
3750
+ <div style="display:flex;gap:8px">
3751
+ <button onclick="approveQuotaReq('${escHtml(x.id)}')" style="padding:7px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${_qT('批准', 'Approve')}</button>
3752
+ <button onclick="rejectQuotaReq('${escHtml(x.id)}')" style="padding:7px 14px;border:1px solid #ef4444;background:#fff;color:#991b1b;border-radius:6px;font-size:12px;cursor:pointer">${_qT('拒绝', 'Reject')}</button>
3753
+ </div>
3754
+ </div>` : ''}
3755
+ </div>`
3756
+ }
3757
+
3758
+ const body = `
3759
+ <div style="max-width:640px;margin:0 auto">
3760
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
3761
+ <div style="font-size:18px;font-weight:700">🎟️ ${_qT('建任务额度审核', 'Build-task quota review')}</div>
3762
+ <a href="#admin" style="font-size:12px;color:#4338ca;text-decoration:none">← ${_qT('返回', 'Back')}</a>
3763
+ </div>
3764
+ <div style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
3765
+ ${filterBtn('pending', _qT('待审核', 'Pending'))}${filterBtn('approved', _qT('已批准', 'Approved'))}${filterBtn('rejected', _qT('已拒绝', 'Rejected'))}${filterBtn('all', _qT('全部', 'All'))}
3766
+ </div>
3767
+ ${reqs.length ? reqs.map(card).join('') : `<div style="font-size:13px;color:#9ca3af">${_qT('暂无申请', 'No requests')}</div>`}
3768
+ </div>`
3769
+ app.innerHTML = shell(body, 'admin')
3770
+ }
3771
+
3772
+ window.loadQuotaUsage = async (id) => {
3773
+ const box = document.getElementById('usage-' + id)
3774
+ if (box) box.innerHTML = t('加载中...')
3775
+ const r = await GET('/admin/quota-requests/' + encodeURIComponent(id)).catch(() => null)
3776
+ if (box) box.innerHTML = (r && !r.error)
3777
+ ? `${_qT('申请人近 24h 已建任务', 'Requester tasks in last 24h')}: <b>${escHtml(String(r.requester_usage_24h))}</b>`
3778
+ : ((r && r.error) || _qT('加载失败', 'Failed'))
3779
+ }
3780
+ window.approveQuotaReq = async (id) => {
3781
+ const v = (p) => (document.getElementById(p + '-' + id)?.value || '').trim()
3782
+ const body = { extra_count: Number(v('ap-count')) || undefined, duration_hours: Number(v('ap-dur')) || undefined, approval_note: v('ap-note') || undefined }
3783
+ const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/approve', body)
3784
+ if (r && r.error) { toast$(r.error_code === 'SELF_DECISION' ? _qT('不能审核自己的申请', 'Cannot decide your own request') : (r.error || _qT('批准失败', 'Approve failed'))); return }
3785
+ toast$(_qT('已批准', 'Approved'))
3786
+ renderAdminBuildTaskQuota(document.getElementById('app'), 'pending')
3787
+ }
3788
+ window.rejectQuotaReq = async (id) => {
3789
+ const note = (document.getElementById('rj-note-' + id)?.value || '').trim()
3790
+ const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/reject', { rejection_note: note || undefined })
3791
+ if (r && r.error) { toast$(r.error_code === 'SELF_DECISION' ? _qT('不能审核自己的申请', 'Cannot decide your own request') : (r.error || _qT('拒绝失败', 'Reject failed'))); return }
3792
+ toast$(_qT('已拒绝', 'Rejected'))
3793
+ renderAdminBuildTaskQuota(document.getElementById('app'), 'pending')
3794
+ }
3795
+ window.revokeQuotaReq = async (id) => {
3796
+ const r = await POST('/admin/quota-requests/' + encodeURIComponent(id) + '/revoke', {})
3797
+ if (r && r.error) { toast$(r.error || _qT('撤销失败', 'Revoke failed')); return }
3798
+ toast$(_qT('已撤销', 'Revoked'))
3799
+ renderAdminBuildTaskQuota(document.getElementById('app'), 'approved')
3800
+ }
3801
+
3315
3802
  async function renderAdminKPI(app) {
3316
3803
  if (!state.user) { renderLogin(); return }
3317
3804
  if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
@@ -3466,22 +3953,17 @@ async function renderAdminDashboard(app) {
3466
3953
  ])
3467
3954
  const tk = data.tokenomics || {}
3468
3955
  const kpiTokenomics1 = kpiGrid([
3469
- { label: t('全球基金池'), value: Number(tk.pool_balance || 0).toFixed(2), unit: 'WAZ' },
3470
- { label: t('待结 Score'), value: Number(tk.scores_pending || 0).toFixed(0) },
3471
- { label: t('管理津贴池'), value: Number(tk.management_bonus || 0).toFixed(2), unit: 'WAZ' },
3472
- ])
3473
- const kpiTokenomics2 = kpiGrid([
3474
3956
  { label: t('累计分享分润'),value: Number(tk.commission_total || 0).toFixed(2), unit: 'WAZ' },
3475
- { label: t('累计匹配发放'),value: Number(tk.binary_waz_total || 0).toFixed(2), unit: 'WAZ' },
3476
3957
  { label: t('PV 待处理'), value: tk.ledger_pending ?? 0 },
3958
+ { label: t('参与记录用户'), value: tk.dirty_users ?? 0 },
3477
3959
  ])
3960
+ const kpiTokenomics2 = ''
3478
3961
  // 异常告警 banner — 多条件聚合
3479
3962
  const alerts = []
3480
3963
  if ((data.active_verifiers ?? 0) < 5) alerts.push({ icon: '⚠️', color: '#dc2626', text: t('活跃审核员不足 5 人 — 请尽快批准申请'), href: '#admin/verifier-applications' })
3481
3964
  if ((data.disputes_open ?? 0) > 10) alerts.push({ icon: '⚖️', color: '#dc2626', text: t('待处理争议') + ' > 10:' + data.disputes_open, href: '#admin/disputes' })
3482
3965
  if ((data.verifier_apps_pending ?? 0) > 5) alerts.push({ icon: '📥', color: '#d97706', text: t('待审申请积压') + ': ' + data.verifier_apps_pending, href: '#admin/verifier-applications' })
3483
3966
  if ((data.users_suspended ?? 0) > (data.users ?? 0) * 0.05) alerts.push({ icon: '⛔', color: '#d97706', text: t('暂停账户占比 > 5%') + ': ' + data.users_suspended, href: '#admin/users' })
3484
- if (Number(tk.pool_balance || 0) < 100) alerts.push({ icon: '💰', color: '#dc2626', text: t('基金池水位告急') + ': ' + Number(tk.pool_balance || 0).toFixed(0) + ' WAZ', href: '#admin/tokenomics' })
3485
3967
  const lowVerifierWarn = alerts.length > 0 ? `
3486
3968
  <div style="margin-bottom:14px">
3487
3969
  <div style="font-size:11px;color:#6b7280;margin-bottom:4px;font-weight:600">🚨 ${t('需要关注')} (${alerts.length})</div>
@@ -3514,7 +3996,7 @@ async function renderAdminDashboard(app) {
3514
3996
  </div>
3515
3997
  <div style="font-size:13px;color:#6b7280;margin:16px 0 8px">⚛ ${t('Tokenomics')}</div>
3516
3998
  <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
3517
- ${quickAction('#admin/tokenomics', '', t('积分基金 / Tier 配置 / 高额榜'))}
3999
+ ${quickAction('#admin/tokenomics', '', t('协议运营 / 注册门控 / 佣金榜'))}
3518
4000
  </div>
3519
4001
  <div style="font-size:13px;color:#6b7280;margin:16px 0 8px">🔐 ${t('安全与审计')}</div>
3520
4002
  <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
@@ -3542,7 +4024,7 @@ async function renderAdminDashboard(app) {
3542
4024
  ${kpi4}
3543
4025
  ${sectionTitle('📥', t('卖家配额'), '#d97706')}
3544
4026
  ${kpi5}
3545
- ${sectionTitle('', t('Tokenomics — 两轨基金'), '#9333ea')}
4027
+ ${sectionTitle('', t('协议运营'), '#9333ea')}
3546
4028
  ${kpiTokenomics1}
3547
4029
  ${kpiTokenomics2}
3548
4030
  ${sectionTitle('⚡', t('快捷操作'), '#4f46e5')}
@@ -3894,10 +4376,6 @@ async function renderAdminUserDetail(app, userId) {
3894
4376
  <div style="font-weight:600;margin-bottom:8px">🎭 ${t('角色 & 权限')}</div>
3895
4377
  <div style="font-size:13px;margin-bottom:6px"><span style="color:#6b7280">${t('当前激活')}</span>: <strong>${b.role}</strong></div>
3896
4378
  <div style="font-size:13px;margin-bottom:8px"><span style="color:#6b7280">${t('全部角色')}</span>: ${(b.roles || []).map(r => `<span style="font-size:11px;background:#f3f4f6;padding:2px 6px;border-radius:8px;margin-right:4px">${r}</span>`).join('')}</div>
3897
- <div style="font-size:13px;display:flex;justify-content:space-between;align-items:center;padding-top:8px;border-top:1px solid #f3f4f6">
3898
- <span><span style="color:#6b7280">🎁 ${t('管理津贴资格')}</span>: ${b.mgmt_bonus_eligible ? `<span style="color:#16a34a">✓ ${t('已授予')}</span>` : `<span style="color:#9ca3af">${t('未授予')}</span>`}</span>
3899
- <button class="btn btn-outline btn-sm" style="font-size:11px;padding:3px 8px" onclick="doMgmtBonusEligibleDetail('${b.id}',${!b.mgmt_bonus_eligible})">${b.mgmt_bonus_eligible ? t('撤销') : t('授予')}</button>
3900
- </div>
3901
4379
  <div style="font-size:13px;padding-top:8px;border-top:1px solid #f3f4f6">
3902
4380
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
3903
4381
  <span><span style="color:#6b7280">🎯 ${t('L1 分享权限')}</span>: ${b.can_l1_share ? `<span style="color:#16a34a">✓ ${t('可拿分享佣金')}</span>` : `<span style="color:#9ca3af">${t('不可')}</span>`}</span>
@@ -4837,7 +5315,7 @@ async function renderRewardsMe(app) {
4837
5315
  `}
4838
5316
 
4839
5317
  <div style="margin-top:20px;font-size:11px;color:#9ca3af;line-height:1.6">
4840
- ${t('协议依据:')} <a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/rfcs/RFC-002-rewards-opt-in.md" target="_blank" style="color:#6b7280">RFC-002</a>
5318
+ ${t('协议依据:')} <a href="https://github.com/webaz-protocol/webaz/blob/main/docs/rfcs/RFC-002-rewards-opt-in.md" target="_blank" style="color:#6b7280">RFC-002</a>
4841
5319
  </div>
4842
5320
  `, 'me')
4843
5321
  }
@@ -4904,12 +5382,12 @@ async function renderOnboarding(app, role) {
4904
5382
  }
4905
5383
 
4906
5384
  const studyDocs = [
4907
- { name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md' },
4908
- { name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/CHARTER.md' },
4909
- { name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/SECURITY.md' },
4910
- { name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md' },
4911
- { name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md' },
4912
- { name: 'MLM-COMPLIANCE.md', desc: t('合规边界'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md' },
5385
+ { name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/META-RULES-FULL.md' },
5386
+ { name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/CHARTER.md' },
5387
+ { name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/webaz-protocol/webaz/blob/main/SECURITY.md' },
5388
+ { name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ECONOMIC-MODEL.md' },
5389
+ { name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md' },
5390
+ { name: 'PARTICIPATION-ATTRIBUTION-COMPLIANCE.md', desc: t('合规边界'), link: 'https://github.com/webaz-protocol/webaz/blob/main/docs/PARTICIPATION-ATTRIBUTION-COMPLIANCE.md' },
4913
5391
  ]
4914
5392
 
4915
5393
  const studySection = studyDocs.map((d, i) => `
@@ -5809,16 +6287,6 @@ async function renderAdminTokenomics(app) {
5809
6287
  app.innerHTML = shell(loading$(), 'admin')
5810
6288
  const data = await GET('/admin/tokenomics')
5811
6289
  if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
5812
- const gf = data.global_fund || {}
5813
- const mb = data.management_bonus_pool || {}
5814
-
5815
- const tierTable = (data.tier_config || []).map(t => `
5816
- <tr style="border-bottom:1px solid #f3f4f6">
5817
- <td style="padding:6px 8px">tier ${t.tier}</td>
5818
- <td style="padding:6px 8px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
5819
- <td style="padding:6px 8px;text-align:right;font-weight:600">${t.score_per_hit}</td>
5820
- <td style="padding:6px 8px;text-align:center">${t.active ? '✓' : '⏸'}</td>
5821
- </tr>`).join('')
5822
6290
 
5823
6291
  const commRows = (data.top_commission || []).map(r => `
5824
6292
  <div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
@@ -5826,104 +6294,27 @@ async function renderAdminTokenomics(app) {
5826
6294
  <div style="font-weight:700;color:#059669">${Number(r.earned).toFixed(2)} WAZ</div>
5827
6295
  </div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
5828
6296
 
5829
- const binaryRows = (data.top_binary || []).map(r => `
5830
- <div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
5831
- <div>${escHtml(r.name || r.user_id)} · ${r.hits}${t('次')} · ${r.score_total}${t('分')}</div>
5832
- <div style="font-weight:700;color:#0891b2">${Number(r.waz_total).toFixed(2)} WAZ</div>
5833
- </div>`).join('') || `<div style="text-align:center;color:#9ca3af;padding:12px;font-size:12px">${t('暂无数据')}</div>`
5834
-
5835
6297
  app.innerHTML = shell(`
5836
- <h1 class="page-title">⚛ Tokenomics</h1>
6298
+ <h1 class="page-title">⚙ ${t('协议运营')}</h1>
5837
6299
  <div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
5838
6300
  <div id="tk-msg"></div>
5839
6301
 
5840
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">💰 ${t('全球基金池')}</h2>
5841
- <div class="card" style="margin-bottom:12px">
5842
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:13px">
5843
- <div><div style="color:#6b7280;font-size:11px">${t('池子余额')}</div><div style="font-size:18px;font-weight:700">${Number(gf.pool_balance ?? 0).toFixed(2)} WAZ</div></div>
5844
- <div><div style="color:#6b7280;font-size:11px">${t('待结 Score')}</div><div style="font-size:18px;font-weight:700">${Number(gf.total_scores_pending ?? 0).toFixed(0)}</div></div>
5845
- <div><div style="color:#6b7280;font-size:11px">${t('上次 N')}</div><div style="font-size:18px;font-weight:700">${Number(gf.current_n ?? 0).toFixed(4)}</div></div>
5846
- </div>
5847
- ${gf.last_settled_at ? `<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('上次结算')}: ${fmtTime(gf.last_settled_at)}</div>` : ''}
5848
- </div>
5849
-
5850
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🎁 ${t('管理津贴池')}</h2>
5851
- <div class="card" style="margin-bottom:12px">
5852
- <div style="font-size:18px;font-weight:700">${Number(mb.balance ?? 0).toFixed(2)} WAZ</div>
5853
- <div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('来自协议费 50%,用于大博主匹配团队 10/5/2% 补贴')}</div>
5854
- <div id="mgmt-bonus-control" style="margin-top:12px;padding:10px;background:#fef9c3;border:1px solid #fde047;border-radius:6px">
5855
- <div style="font-size:12px;font-weight:600;margin-bottom:6px">⚠️ ${t('津贴门控')}</div>
5856
- <div id="mgmt-bonus-status" style="font-size:11px;color:#92400e;margin-bottom:8px">${t('加载中...')}</div>
5857
- <button class="btn btn-outline btn-sm" style="font-size:11px" onclick="loadMgmtBonusStatus()">${t('查看资格用户 + 切换开关')}</button>
5858
- </div>
5859
- </div>
5860
-
5861
6302
  <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🚪 ${t('注册门控')}</h2>
5862
6303
  <div class="card" style="margin-bottom:12px">
5863
6304
  <div id="require-ref-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
5864
6305
  <div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:无邀请码不能注册(admin/物流/仲裁/审核员 与 region=china 豁免)')}</div>
5865
6306
  </div>
5866
6307
 
5867
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🎟 ${t('邀请码轮询')}</h2>
5868
- <div class="card" style="margin-bottom:12px">
5869
- <div id="invite-rotation-status" style="font-size:12px;color:#6b7280">${t('加载中...')}</div>
5870
- <div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('开启后:注册页"获取邀请码"按钮可用,依次轮询 xiaohua / mian / holden / jiayi / qingliang 5 位用户的 permanent_code')}</div>
5871
- </div>
5872
-
5873
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">⚙ ${t('Cron 控制 + 紧急操作')}</h2>
6308
+ <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">📒 ${t('参与记录(PV)流水')}</h2>
5874
6309
  <div class="card" style="margin-bottom:12px">
5875
- <div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px">
5876
- <button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
5877
- <button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doTkSettle()">${t('触发匹配结算')}</button>
5878
- <button class="btn btn-outline btn-sm" style="font-size:11px;color:#dc2626;border-color:#dc2626" onclick="if(confirm('${t('确认分发 WAZ?这将清空池子按 N 比例分配')}'))doTkDistribute()">${t('分发 WAZ(清池)')}</button>
5879
- </div>
6310
+ <button class="btn btn-outline btn-sm" style="font-size:11px;margin-bottom:8px" onclick="doTkProcess()">${t('处理 PV 流水')}</button>
5880
6311
  <div style="font-size:12px;color:#6b7280">${t('PV 待处理流水')}: ${data.pv_ledger?.pending ?? 0} ${t('条')} · ${t('待累积 PV')}: ${Number(data.pv_ledger?.pending_pv ?? 0).toLocaleString()}</div>
5881
6312
  </div>
5882
6313
 
5883
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">📊 ${t('Tier 配置')} <span style="font-size:11px;color:#9ca3af">${t('(admin 可调)')}</span></h2>
5884
- <div class="card" style="margin-bottom:12px;padding:0">
5885
- <table style="width:100%;border-collapse:collapse">
5886
- <tr style="background:#f9fafb;font-size:11px;color:#6b7280">
5887
- <th style="padding:6px 8px;text-align:left">Tier</th>
5888
- <th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
5889
- <th style="padding:6px 8px;text-align:right">Score</th>
5890
- <th style="padding:6px 8px;text-align:center">${t('启用')}</th>
5891
- </tr>
5892
- ${tierTable}
5893
- </table>
5894
- <div style="padding:8px 12px;font-size:11px;color:#6b7280">${t('调整:POST /api/admin/tokenomics/tier with body {tier, pv_threshold, score_per_hit, active}')}</div>
5895
- </div>
5896
-
5897
6314
  <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 分享佣金')} (Top 10)</h2>
5898
6315
  <div class="card" style="padding:0;margin-bottom:12px">${commRows}</div>
5899
-
5900
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 匹配收益')} (Top 10)</h2>
5901
- <div class="card" style="padding:0;margin-bottom:12px">${binaryRows}</div>
5902
6316
  `, 'admin')
5903
6317
  setTimeout(loadRequireRefStatus, 100)
5904
- setTimeout(loadInviteRotationStatus, 100)
5905
- }
5906
-
5907
- window.loadInviteRotationStatus = async () => {
5908
- const f = await GET('/system-flags').catch(() => null)
5909
- const el = document.getElementById('invite-rotation-status')
5910
- if (!el) return
5911
- const enabled = !!f?.invite_rotation_enabled
5912
- el.innerHTML = `
5913
- <div style="display:flex;justify-content:space-between;align-items:center">
5914
- <strong>${t('邀请码轮询')}</strong>: ${enabled ? `<span style="color:#16a34a">🟢 ${t('已开启 — 按钮可用')}</span>` : `<span style="color:#dc2626">🔒 ${t('已关闭 — 按钮置灰')}</span>`}
5915
- <button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doToggleInviteRotation(${!enabled})">${enabled ? t('关闭') : t('开启')}</button>
5916
- </div>`
5917
- }
5918
-
5919
- window.doToggleInviteRotation = async (enable) => {
5920
- if (!confirm(enable
5921
- ? t('确认开启邀请码轮询?访客在注册页点按钮可申领下一个 sponsor 邀请码')
5922
- : t('确认关闭邀请码轮询?按钮将置灰')
5923
- )) return
5924
- const res = await POST('/admin/invite-rotation/toggle', { enabled: enable })
5925
- if (res.error) return alert(res.error)
5926
- loadInviteRotationStatus()
5927
6318
  }
5928
6319
 
5929
6320
  window.loadRequireRefStatus = async () => {
@@ -5954,46 +6345,6 @@ window.doTkProcess = async () => {
5954
6345
  setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
5955
6346
  }
5956
6347
 
5957
- window.doTkSettle = async () => {
5958
- const res = await POST('/admin/atomic/run-settlement', {})
5959
- const msg = document.getElementById('tk-msg')
5960
- if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已触发匹配')}: ${res.settled}`)
5961
- setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
5962
- }
5963
-
5964
- window.loadMgmtBonusStatus = async () => {
5965
- const data = await GET('/admin/tokenomics/mgmt-bonus')
5966
- if (data.error) return alert(data.error)
5967
- const el = document.getElementById('mgmt-bonus-status')
5968
- const userRows = (data.eligible_users || []).map(u => `
5969
- <div style="display:flex;justify-content:space-between;padding:4px 6px;font-size:11px;border-bottom:1px solid #fef9c3">
5970
- <div>${escHtml(u.name)} <span style="color:#9ca3af">(L1=${u.l1_count}, 累计佣金 ${Number(u.total_commission).toFixed(2)} WAZ)</span></div>
5971
- <button style="font-size:10px;color:#dc2626;background:none;border:none;cursor:pointer" onclick="doMgmtBonusEligible('${u.id}',false)">${t('撤销')}</button>
5972
- </div>`).join('') || `<div style="font-size:11px;color:#9ca3af;text-align:center;padding:8px">${t('暂无资格用户')}</div>`
5973
- el.innerHTML = `
5974
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
5975
- <span><strong>${t('全局开关')}</strong>: ${data.enabled ? `<span style="color:#16a34a">✓ ${t('已开启')}</span>` : `<span style="color:#dc2626">⊘ ${t('已关闭')}</span>`}</span>
5976
- <button class="btn btn-outline btn-sm" style="font-size:11px" onclick="doMgmtBonusToggle(${!data.enabled})">${data.enabled ? t('关闭') : t('开启')}</button>
5977
- </div>
5978
- <div style="font-size:11px;color:#92400e;margin-bottom:6px">${t('资格用户')} (${data.eligible_count}):</div>
5979
- <div style="background:#fff;border-radius:4px;max-height:200px;overflow-y:auto">${userRows}</div>
5980
- <div style="font-size:10px;color:#92400e;margin-top:6px">${t('在用户详情页可授予/撤销该资格')}</div>`
5981
- }
5982
-
5983
- window.doMgmtBonusToggle = async (enable) => {
5984
- if (!confirm(enable ? t('确认开启管理津贴?') : t('确认关闭管理津贴?(已发放的不回收)'))) return
5985
- const res = await POST('/admin/tokenomics/mgmt-bonus/toggle', { enabled: enable })
5986
- if (res.error) return alert(res.error)
5987
- loadMgmtBonusStatus()
5988
- }
5989
-
5990
- window.doMgmtBonusEligible = async (userId, eligible) => {
5991
- if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
5992
- const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
5993
- if (res.error) return alert(res.error)
5994
- loadMgmtBonusStatus()
5995
- }
5996
-
5997
6348
  window.doL1ShareOverride = async (userId, value) => {
5998
6349
  const labels = { 1: t('强制允许'), 0: t('Auto'), '-1': t('强制禁止') }
5999
6350
  if (!confirm(`${t('设置 L1 分享权限为')}: ${labels[value]}?`)) return
@@ -6002,20 +6353,6 @@ window.doL1ShareOverride = async (userId, value) => {
6002
6353
  renderAdminUserDetail(document.getElementById('app'), userId)
6003
6354
  }
6004
6355
 
6005
- window.doMgmtBonusEligibleDetail = async (userId, eligible) => {
6006
- if (!confirm(eligible ? t('授予该用户管理津贴资格?') : t('撤销该用户管理津贴资格?'))) return
6007
- const res = await POST(`/admin/users/${userId}/mgmt-bonus-eligible`, { eligible })
6008
- if (res.error) { showUserOpMsg(alert$('error', res.error)); return }
6009
- renderAdminUserDetail(document.getElementById('app'), userId)
6010
- }
6011
-
6012
- window.doTkDistribute = async () => {
6013
- const res = await POST('/admin/atomic/distribute', {})
6014
- const msg = document.getElementById('tk-msg')
6015
- if (msg) msg.innerHTML = res.error ? alert$('error', res.error) : alert$('success', `${t('已分发 WAZ')}: ${Number(res.distributed||0).toFixed(2)}`)
6016
- setTimeout(() => renderAdminTokenomics(document.getElementById('app')), 800)
6017
- }
6018
-
6019
6356
  async function renderAdminAudit(app) {
6020
6357
  if (!state.user) { renderLogin(); return }
6021
6358
  if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-audit'); return }
@@ -6517,76 +6854,58 @@ function renderWelcome(app) {
6517
6854
  <section class="w-section" id="w-join-section">
6518
6855
  <h2 style="${H2_STYLE}">${T('成为 webazer', 'Become a webazer')}</h2>
6519
6856
  <p style="${SUB_STYLE}">${T('无论你从哪里来,你已经在路上了。', "Wherever you come from, you're already on the way.")}</p>
6520
- <!-- 横版排列:3 张卡纵向堆叠,每张卡内部左标题 + 右操作 -->
6857
+ <!-- 三组意图:① 立即开始(注册/贡献) 保持联系(邮箱+社区) ③ 有想法(反馈/提任务) — 6 卡精简为 3,功能零丢失 -->
6521
6858
  <div style="display:flex;flex-direction:column;gap:14px">
6522
- <div class="w-card w-join-card" style="flex-direction:column;align-items:stretch">
6523
- <div style="margin-bottom:14px">
6524
- <div class="w-card-title">📧 ${T('申请加入创世团(深度参与)', 'Apply to join the Genesis Cohort (deep dive)')}</div>
6525
- <div class="w-card-desc">${T('留下邮箱 + 期望身份 · 上线第一时间联系你,我们会主动跟进', "Leave your email + desired role · we'll reach out at launch and follow up")}</div>
6526
- </div>
6527
- <div style="display:flex;flex-direction:column;gap:10px">
6528
- <input id="w-email" class="w-input" type="email" placeholder="your@email.com" autocomplete="email" style="margin-bottom:0">
6529
- <!-- honeypot -->
6530
- <input id="w-email-hp" name="website" autocomplete="off" tabindex="-1" style="display:none" aria-hidden="true">
6531
- <select id="w-role-pref" class="w-input" style="margin-bottom:0">
6532
- <option value="">${T('我想以什么身份开始?(可选)', 'How would you like to start? (optional)')}</option>
6533
- <option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
6534
- <option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
6535
- <option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
6536
- <option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
6537
- <option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
6538
- <option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
6539
- </select>
6540
- <textarea id="w-note" class="w-input" rows="3" maxlength="500" placeholder="${T('补充说明(可选):你希望我们告诉你什么?有什么期待?', 'Notes (optional): what do you want to hear from us? Any expectations?')}" style="margin-bottom:0;resize:vertical;font-family:inherit"></textarea>
6541
- <button class="w-btn-full w-btn-primary" onclick="submitWelcomeEmail()" style="padding:12px 20px">${T('申请加入', 'Submit application')}</button>
6542
- <div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
6543
- </div>
6544
- </div>
6859
+ <!-- 立即开始 — 主转化:注册 + 公开任务板(含贡献记录诚实披露) -->
6545
6860
  <div class="w-card w-join-card">
6546
6861
  <div class="w-join-card-left">
6547
- <div class="w-card-title">🛠 ${T('想直接改进 WebAZ?', 'Want to improve WebAZ directly?')}</div>
6548
- <div class="w-card-desc">${T('公开任务板:浏览 / 认领可做的任务,或提个新任务。建议无需登录;认领与提交需登录。只有 canonical 仓库被合并的 PR(或维护者认可的 issue / task / RFC)才进入贡献记录 —— sandbox、本地草稿、普通购物 / 分享都不是正式贡献。', 'Public task board: browse / claim tasks, or propose a new one. Suggesting needs no login; claiming & submitting need login. Only a merged PR on the canonical repo (or a maintainer-recognized issue / task / RFC) enters the contribution record — a sandbox run, a local draft, or ordinary shopping / sharing is not formal contribution.')}</div>
6862
+ <div class="w-card-title">🚀 ${T('立即开始', 'Start now')}</div>
6863
+ <div class="w-card-desc">${T('准备好了就进入协议;或浏览公开任务板参与建设。建议无需登录,认领与提交需登录。只有 canonical 仓库被合并的 PR(或维护者认可的 issue / task / RFC)才进入贡献记录 —— sandbox、本地草稿、普通购物 / 分享都不是正式贡献。', 'Ready? Step into the protocol — or browse the public task board to help build. Suggesting needs no login; claiming & submitting do. Only a merged PR on the canonical repo (or a maintainer-recognized issue / task / RFC) enters the contribution record — a sandbox run, a local draft, or ordinary shopping / sharing is not formal contribution.')}</div>
6549
6864
  </div>
6550
6865
  <div class="w-join-card-right">
6866
+ <button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
6551
6867
  <a class="w-btn-full w-btn-outline" href="#contribute/tasks" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('公开任务板', 'Public task board')}</a>
6552
- <a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
6553
6868
  </div>
6554
6869
  </div>
6555
- <div class="w-card w-join-card">
6556
- <div class="w-join-card-left">
6557
- <div class="w-card-title">💡 ${T('随便逛逛?提个建议', 'Just browsing? Drop an idea')}</div>
6558
- <div class="w-card-desc">${T('匿名 · 无需邮箱 · 一句话想法 / 痛点 / bug 都行', 'Anonymous · no email · a quick idea / pain point / bug')}</div>
6559
- </div>
6560
- <div class="w-join-card-right">
6561
- <button class="w-btn-full w-btn-outline" onclick="openIdeaSheet()">${T('我有建议', 'I have an idea')}</button>
6562
- </div>
6563
- </div>
6564
- <div class="w-card w-join-card">
6565
- <div class="w-join-card-left">
6566
- <div class="w-card-title">💬 ${T('加入社区', 'Join the community')}</div>
6567
- <div class="w-card-desc">${T('找到同路人,开始对话', 'Find peers, start the conversation')}</div>
6568
- </div>
6569
- <div class="w-join-card-right">
6570
- <a class="w-btn-full w-btn-outline" href="#" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('加入 Discord', 'Join Discord')}</a>
6571
- <a class="w-btn-full w-btn-outline" href="https://t.me/webazer" target="_blank" rel="noopener noreferrer" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('加入 Telegram', 'Join Telegram')}</a>
6572
- </div>
6573
- </div>
6574
- <div class="w-card w-join-card">
6575
- <div class="w-join-card-left">
6576
- <div class="w-card-title">📧 ${T('邮件联系', 'Email us')}</div>
6577
- <div class="w-card-desc">${T('合作 / 反馈 / 合规咨询', 'Partnerships / feedback / compliance')}</div>
6870
+ <!-- ② 保持联系 — 上线通知(邮箱+期望身份) + 社区/邮件;w-email / w-role-pref / w-email-hp / w-email-msg 保留(顶部角色 CTA 依赖) -->
6871
+ <div class="w-card w-join-card" style="flex-direction:column;align-items:stretch">
6872
+ <div style="margin-bottom:14px">
6873
+ <div class="w-card-title">📧 ${T('保持联系(上线通知)', 'Stay in touch (launch notice)')}</div>
6874
+ <div class="w-card-desc">${T('留下邮箱 + 期望身份,上线第一时间通知你;或在社区先聊起来。', "Leave your email + desired role and we'll notify you at launch; or start chatting in the community.")}</div>
6578
6875
  </div>
6579
- <div class="w-join-card-right">
6580
- <a class="w-btn-full w-btn-outline" href="mailto:contact@webaz.xyz" style="display:flex;align-items:center;justify-content:center;text-decoration:none;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px">contact@webaz.xyz</a>
6876
+ <div style="display:flex;flex-direction:column;gap:10px">
6877
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
6878
+ <input id="w-email" class="w-input" type="email" placeholder="your@email.com" autocomplete="email" style="margin-bottom:0;flex:2;min-width:180px">
6879
+ <select id="w-role-pref" class="w-input" style="margin-bottom:0;flex:1;min-width:150px">
6880
+ <option value="">${T('期望身份(可选)', 'Role (optional)')}</option>
6881
+ <option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
6882
+ <option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
6883
+ <option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
6884
+ <option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
6885
+ <option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
6886
+ <option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
6887
+ </select>
6888
+ </div>
6889
+ <!-- honeypot -->
6890
+ <input id="w-email-hp" name="website" autocomplete="off" tabindex="-1" style="display:none" aria-hidden="true">
6891
+ <button class="w-btn-full w-btn-primary" onclick="submitWelcomeEmail()" style="padding:12px 20px">${T('通知我', 'Notify me')}</button>
6892
+ <div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
6893
+ <div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:2px">
6894
+ <a class="w-btn-full w-btn-outline" href="#" style="display:flex;align-items:center;justify-content:center;text-decoration:none;flex:1;min-width:90px">Discord</a>
6895
+ <a class="w-btn-full w-btn-outline" href="https://t.me/webazer" target="_blank" rel="noopener noreferrer" style="display:flex;align-items:center;justify-content:center;text-decoration:none;flex:1;min-width:90px">Telegram</a>
6896
+ <a class="w-btn-full w-btn-outline" href="mailto:contact@webaz.xyz" style="display:flex;align-items:center;justify-content:center;text-decoration:none;flex:1;min-width:90px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12px">${T('邮件', 'Email')}</a>
6897
+ </div>
6581
6898
  </div>
6582
6899
  </div>
6900
+ <!-- ③ 有想法 — 匿名一句话反馈 + 提具体任务 -->
6583
6901
  <div class="w-card w-join-card">
6584
6902
  <div class="w-join-card-left">
6585
- <div class="w-card-title">🚀 ${T('立即注册', 'Sign up now')}</div>
6586
- <div class="w-card-desc">${T('已经准备好了?直接进入协议。', 'Ready already? Step into the protocol.')}</div>
6903
+ <div class="w-card-title">💡 ${T('有想法?', 'Got an idea?')}</div>
6904
+ <div class="w-card-desc">${T('匿名 · 无需邮箱 · 一句话想法 / 痛点 / bug;或提一个具体的新任务。', 'Anonymous · no email · a quick idea / pain point / bug; or propose a concrete new task.')}</div>
6587
6905
  </div>
6588
6906
  <div class="w-join-card-right">
6589
- <button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
6907
+ <button class="w-btn-full w-btn-outline" onclick="openIdeaSheet()">${T('我有建议', 'I have an idea')}</button>
6908
+ <a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
6590
6909
  </div>
6591
6910
  </div>
6592
6911
  </div>
@@ -6597,8 +6916,8 @@ function renderWelcome(app) {
6597
6916
  <div>© 2026 webaz</div>
6598
6917
  <div>${T('开放协议 · Agent 原生 · DAO 治理', 'Open Protocol · Agent-Native · DAO Governance')}</div>
6599
6918
  <div style="margin-top:10px">
6600
- <a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
6601
- <a href="https://github.com/seasonsagents-art/webaz" target="_blank" rel="noopener">GitHub</a>
6919
+ <a href="https://github.com/webaz-protocol/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
6920
+ <a href="https://github.com/webaz-protocol/webaz" target="_blank" rel="noopener">GitHub</a>
6602
6921
  <a href="${WP_URL}" target="_blank" rel="noopener">${T('协议白皮书', 'Whitepaper')}</a>
6603
6922
  <a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
6604
6923
  </div>
@@ -6659,7 +6978,7 @@ function renderWelcome(app) {
6659
6978
  alternateName: 'WebAZ',
6660
6979
  url: location.origin,
6661
6980
  description: 'Agent-native decentralized commerce protocol. State-machine transactions; explicit sign-offs at each transition; auto-timeout-default. Pre-launch.',
6662
- sameAs: ['https://github.com/seasonsagents-art/webaz'],
6981
+ sameAs: ['https://github.com/webaz-protocol/webaz'],
6663
6982
  },
6664
6983
  {
6665
6984
  '@type': 'Service',
@@ -6736,8 +7055,8 @@ async function renderGovernanceOnboarding(app) {
6736
7055
  <section style="border-top:1px solid #e4e4e7;padding-top:20px;color:#71717a;font-size:13px;line-height:1.7">
6737
7056
  <p style="margin:0 0 8px"><strong>${T('完整规范', 'Full spec')}:</strong></p>
6738
7057
  <ul style="margin:0;padding-left:20px">
6739
- <li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md" target="_blank" rel="noopener" style="color:#1d4ed8">GOVERNANCE-ONBOARDING.md</a> — ${T('资格 / 流程 / 卸任 / 申诉', 'eligibility / flow / resignation / appeal')}</li>
6740
- <li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md" target="_blank" rel="noopener" style="color:#1d4ed8">ARBITRATION-PLAYBOOK.md</a> — ${T('arbitrator 决策框架 + 5 模拟案例', 'arbitrator decision framework + 5 simulated cases')}</li>
7058
+ <li><a href="https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md" target="_blank" rel="noopener" style="color:#1d4ed8">GOVERNANCE-ONBOARDING.md</a> — ${T('资格 / 流程 / 卸任 / 申诉', 'eligibility / flow / resignation / appeal')}</li>
7059
+ <li><a href="https://github.com/webaz-protocol/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md" target="_blank" rel="noopener" style="color:#1d4ed8">ARBITRATION-PLAYBOOK.md</a> — ${T('arbitrator 决策框架 + 5 模拟案例', 'arbitrator decision framework + 5 simulated cases')}</li>
6741
7060
  <li>${T('机读 JSON 端点', 'Machine-readable JSON endpoint')}: <code style="background:#f4f4f5;padding:2px 6px;border-radius:4px">/api/governance/onboarding-stats</code></li>
6742
7061
  </ul>
6743
7062
  </section>
@@ -6955,8 +7274,12 @@ window.contributeSetLang = (lang) => {
6955
7274
  function contributeLangSwitchHTML(T) {
6956
7275
  const en = window._lang === 'en'
6957
7276
  const btn = (lang, label, active) => `<button type="button" ${active ? 'disabled' : `onclick="contributeSetLang('${lang}')"`} style="border:0;background:${active ? '#18181B' : 'transparent'};color:${active ? '#fff' : '#71717A'};padding:5px 10px;border-radius:7px;font-size:12px;font-weight:600;cursor:${active ? 'default' : 'pointer'}">${label}</button>`
7277
+ // 返回目标随登录态:已登录回应用首页(roleHome),未登录回欢迎页。
7278
+ // 之前硬编码 #welcome → 登录用户落到预登录营销页,看起来像被退出登录。
7279
+ const home = state.user ? roleHome(state.user.role) : '#welcome'
7280
+ const homeLabel = state.user ? T('返回首页', 'Home') : T('WebAZ 欢迎页', 'WebAZ Welcome')
6958
7281
  return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:10px;border-bottom:1px solid #e4e4e7">
6959
- <a href="#welcome" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${T('WebAZ 欢迎页', 'WebAZ Welcome')}</a>
7282
+ <a href="${home}" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${homeLabel}</a>
6960
7283
  <div role="group" aria-label="${T('语言切换', 'Language switch')}" style="display:flex;align-items:center;gap:2px;border:1px solid #e4e4e7;border-radius:9px;padding:2px;background:#fff">
6961
7284
  ${btn('zh', '中文', !en)}${btn('en', 'EN', en)}
6962
7285
  </div>
@@ -7020,7 +7343,7 @@ async function renderContributeTasks(app) {
7020
7343
  return `<div onclick="location.hash='#contribute/tasks/${_cEsc(task.task_id)}'" style="background:#fff;border:1px solid #e4e4e7;border-radius:10px;padding:14px;margin-bottom:10px;cursor:pointer">
7021
7344
  <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
7022
7345
  <div style="font-weight:600;color:#18181B;font-size:15px">${_cEsc(task.title)}</div>
7023
- ${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
7346
+ ${m.claimability === 'auto_claimable' ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
7024
7347
  </div>
7025
7348
  <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px">
7026
7349
  ${task.area ? `<span style="font-size:11px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
@@ -7058,7 +7381,7 @@ async function renderContributeTaskDetail(app, id) {
7058
7381
  CONTRIBUTE_PROMPT_STATE.text = buildContributeAgentPrompt(task, cct, T)
7059
7382
  const section = (icon, title, inner) => `<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:18px;margin-bottom:14px">
7060
7383
  <h3 style="margin:0 0 10px;color:#18181B;font-size:16px">${icon} ${title}</h3>${inner}</section>`
7061
- const claimBtn = m.auto_claimable
7384
+ const claimBtn = m.claimability === 'auto_claimable'
7062
7385
  ? (state.apiKey
7063
7386
  ? `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">✋ ${T('认领此任务', 'Claim this task')}</button>`
7064
7387
  : `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#fff;color:#18181B;border:1px solid #6366f1;border-radius:8px;font-size:14px;cursor:pointer">🔑 ${T('登录后认领', 'Log in to claim')}</button><div style="font-size:11px;color:#6b7280;margin-top:6px">${T('认领需要登录(真人 Passkey 账户);浏览器不会自动执行 GitHub 操作。', 'Claiming requires login (a real Passkey account); the browser performs no automatic GitHub action.')}</div>`)
@@ -7071,7 +7394,7 @@ async function renderContributeTaskDetail(app, id) {
7071
7394
  ${task.area ? `<span style="font-size:12px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
7072
7395
  ${m.task_type ? `<span style="font-size:12px;color:#6b7280">🔖 ${_cEsc(m.task_type)}</span>` : ''}
7073
7396
  ${_cRiskBadge(m.risk_level, T)} ${_cDuration(m.estimated_duration, T)}
7074
- ${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
7397
+ ${m.claimability === 'auto_claimable' ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
7075
7398
  </div>
7076
7399
  ${_cBoundaryHTML(task.value_boundary || j.value_boundary, T)}
7077
7400
 
@@ -7165,7 +7488,7 @@ window.submitWelcomeEmail = async () => {
7165
7488
  const email = document.getElementById('w-email')?.value?.trim() || ''
7166
7489
  const hp = document.getElementById('w-email-hp')?.value || '' // honeypot
7167
7490
  const rolePref = document.getElementById('w-role-pref')?.value || ''
7168
- const note = (document.getElementById('w-note')?.value || '').trim().slice(0, 500)
7491
+ const note = (document.getElementById('w-note')?.value || '').trim().slice(0, 500) // note field retired in welcome consolidation; tolerated if absent
7169
7492
  const msg = document.getElementById('w-email-msg')
7170
7493
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请输入有效邮箱')}</span>`; return }
7171
7494
  if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交中…')}</span>`
@@ -7178,8 +7501,8 @@ window.submitWelcomeEmail = async () => {
7178
7501
  })
7179
7502
  if (r?.error) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${r.error}</span>`; return }
7180
7503
  if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('已收到,上线时第一时间通知你')}</span>`
7181
- document.getElementById('w-email').value = ''
7182
- document.getElementById('w-note').value = ''
7504
+ const emailEl = document.getElementById('w-email'); if (emailEl) emailEl.value = ''
7505
+ const noteEl = document.getElementById('w-note'); if (noteEl) noteEl.value = '' // retired field — guard so clear never throws
7183
7506
  // role-pref 保留(用户已表达过的偏好,刷新页面前都留着)
7184
7507
  }
7185
7508
 
@@ -7213,13 +7536,21 @@ window.openParticipateSheet = (defaultTab) => {
7213
7536
  <button onclick="(closeSheet(),openAuthSheet('${defaultTab||'login'}'))" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
7214
7537
  <span>🔑 ${t('注册 / 登录')}</span><span style="font-size:13px;opacity:0.6">›</span>
7215
7538
  </button>
7216
- <button onclick="(closeSheet(),navigate('#contribute/tasks'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
7217
- <span>🛠 ${t('浏览公开任务板')}</span><span style="font-size:13px;opacity:0.6">›</span>
7218
- </button>
7219
- <button onclick="(closeSheet(),navigate('#contribute/tasks/suggest'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
7220
- <span>💡 ${t('提建议(无需登录)')}</span><span style="font-size:13px;opacity:0.6">›</span>
7221
- </button>
7222
- <div style="font-size:10px;color:#9ca3af;text-align:center;margin-top:14px;line-height:1.5"><a href="#welcome" onclick="closeSheet()" style="color:#9ca3af;text-decoration:none">${t('了解协议设计 · 多种角色 · 元规则')}</a></div>
7539
+ <!-- 次级探索路径(浏览任务板 / 提建议 / 了解协议)收进可展开「了解更多」,突出主 CTA -->
7540
+ <details style="border:1px solid #e5e7eb;border-radius:10px;overflow:hidden">
7541
+ <summary style="padding:13px 14px;font-size:14px;font-weight:600;color:#4338ca;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between">
7542
+ <span>📖 ${t('了解更多')}</span><span style="font-size:13px;opacity:0.6">▾</span>
7543
+ </summary>
7544
+ <div style="padding:2px 10px 12px;display:flex;flex-direction:column;gap:8px">
7545
+ <button onclick="(closeSheet(),navigate('#contribute/tasks'))" class="btn btn-outline" style="width:100%;padding:12px;font-size:14px;font-weight:600;border-radius:9px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #c7d2fe;color:#4338ca">
7546
+ <span>🛠 ${t('浏览公开任务板')}</span><span style="font-size:13px;opacity:0.6">›</span>
7547
+ </button>
7548
+ <button onclick="(closeSheet(),navigate('#contribute/tasks/suggest'))" class="btn btn-outline" style="width:100%;padding:12px;font-size:14px;font-weight:600;border-radius:9px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #c7d2fe;color:#4338ca">
7549
+ <span>💡 ${t('提建议(无需登录)')}</span><span style="font-size:13px;opacity:0.6">›</span>
7550
+ </button>
7551
+ <a href="#welcome" onclick="closeSheet()" style="text-align:center;font-size:12px;color:#9ca3af;text-decoration:none;padding:6px 4px 2px;line-height:1.5">${t('了解协议设计 · 多种角色 · 元规则')} ›</a>
7552
+ </div>
7553
+ </details>
7223
7554
  </div>
7224
7555
  `, { maxWidth: 460 })
7225
7556
  }
@@ -7312,10 +7643,7 @@ window.openAuthSheet = (defaultTab) => {
7312
7643
  </div>
7313
7644
  <div class="form-group">
7314
7645
  <label class="form-label">${t('邀请码')} <span style="color:#dc2626">*</span></label>
7315
- <div style="display:flex;gap:6px;align-items:stretch">
7316
- <input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;flex:1">
7317
- <button id="btn-fetch-ref" type="button" disabled title="${t('该功能默认关闭,由管理员开启后可用')}" onclick="doFetchInviteCode()" style="white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed">${t('获取邀请码')}</button>
7318
- </div>
7646
+ <input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;width:100%">
7319
7647
  <div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码;没有就联系老用户拿邀请链接')}</div>
7320
7648
  </div>
7321
7649
  <div class="form-group">
@@ -7390,32 +7718,23 @@ async function renderPromoter(app) {
7390
7718
  const leftPv = Number(atomic.total_left_pv || 0)
7391
7719
  const rightPv = Number(atomic.total_right_pv || 0)
7392
7720
  const weak = Math.min(leftPv, rightPv)
7393
- const tiersArr = atomic.tier_config || []
7394
- const nextTier = tiersArr.find(x => x.pv_threshold > weak) || tiersArr[tiersArr.length - 1]
7395
- const nextProgress = nextTier ? Math.min(100, (weak / nextTier.pv_threshold) * 100) : 0
7396
7721
 
7397
- // ─── ① 顶部 KPI — 紧张地区改为"奖励时间线"(合规:不诱导金钱大字,仅按时间显示已发生事实) ───
7398
- const totalEarnings = data.earnings.grand_total + (atomic.score?.settled_waz || 0)
7399
- const last30 = Number(data.projection?.last_30_commission || 0) + Number(data.projection?.last_30_atomic_waz || 0)
7722
+ // ─── ① 顶部 KPI — 紧张地区改为时间线(合规:不诱导金钱大字,仅按时间显示已发生事实) ───
7723
+ const totalEarnings = data.earnings.grand_total
7724
+ const last30 = Number(data.projection?.last_30_commission || 0)
7400
7725
  const growth = data.projection?.growth_rate
7401
- const pending = atomic.score?.pending_score || 0
7402
7726
  const _mlmMax = Number(state.user?.region_max_levels ?? 1)
7403
7727
  const _kpiRestricted = _mlmMax <= 1
7404
- // 两个语义独立的 gate(三级奖励 ≠ PV 双轨系统)—— 2026-06-04 已解耦:
7405
- // - _kpiRestricted (max ≤ 1):紧张地区 KPI 改时间线 / team 隐 WAZ
7406
- // - _pvAllowed:PV 双轨/匹配系统是否开启,读 region_pv_enabled(独立旋钮,不再绑 max≥3)
7407
- // commission 层级(max_levels) 与 PV 系统(pv_enabled) 分离:可单独开某辖区到 L2/L3 而 PV 仍关。
7408
- const _pvAllowed = Number(state.user?.region_pv_enabled ?? 0) === 1
7728
+ // 匹配奖励引擎已切除(#401):只展示中性参与记录(PV),不再有 Score/档位等奖励视图。
7729
+ // _recordingOn:中性参与记录(PV ledger/聚合)是否开启 —— data.gates.participation_recording_active(默认 true)。
7730
+ const _recordingOn = data.gates?.participation_recording_active !== false
7409
7731
  // 紧张地区:拼装最近奖励时间线(commission + 匹配 binary 混合,按时间倒序取 5 条)
7410
7732
  let kpiBar = ''
7411
7733
  if (_kpiRestricted) {
7412
7734
  const _cm = (data.recent || []).slice(0, 10).map(r => ({
7413
7735
  ts: r.created_at, label: `L${r.level} ${t('佣金')}`, amount: Number(r.amount || 0),
7414
7736
  }))
7415
- const _bn = (atomic.recent_binary || []).slice(0, 10).map(r => ({
7416
- ts: r.created_at, label: `tier ${r.tier} ${t('发展奖')}`, amount: Number(r.waz_amount || 0),
7417
- }))
7418
- const _merged = [..._cm, ..._bn].sort((a, b) => (b.ts || '').localeCompare(a.ts || '')).slice(0, 5)
7737
+ const _merged = _cm.sort((a, b) => (b.ts || '').localeCompare(a.ts || '')).slice(0, 5)
7419
7738
  kpiBar = `
7420
7739
  <div class="card" style="padding:12px 14px;margin-bottom:16px">
7421
7740
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
@@ -7447,14 +7766,14 @@ async function renderPromoter(app) {
7447
7766
  <div style="font-size:10px;color:${growth == null ? '#9ca3af' : (growth >= 0 ? '#16a34a' : '#dc2626')}">${growth == null ? t('新用户期') : (growth >= 0 ? '↑' : '↓') + ' ' + Math.abs(growth*100).toFixed(0) + '%'}</div>
7448
7767
  </div>
7449
7768
  <div class="card" style="text-align:center;padding:10px 6px">
7450
- <div style="font-size:11px;color:#6b7280">⏳ ${t('待结')}</div>
7451
- <div style="font-size:17px;font-weight:700;color:#d97706;margin-top:2px">${Math.round(pending)}</div>
7452
- <div style="font-size:10px;color:#9ca3af">Score</div>
7769
+ <div style="font-size:11px;color:#6b7280">📒 ${t('参与积分')}</div>
7770
+ <div style="font-size:17px;font-weight:700;color:#4338ca;margin-top:2px">${(leftPv + rightPv).toLocaleString()}</div>
7771
+ <div style="font-size:10px;color:#9ca3af">PV</div>
7453
7772
  </div>
7454
7773
  <div class="card" style="text-align:center;padding:10px 6px">
7455
- <div style="font-size:11px;color:#6b7280">🌟 ${t('下一档')}</div>
7456
- <div style="font-size:17px;font-weight:700;color:#7c3aed;margin-top:2px">${nextTier ? nextProgress.toFixed(0) + '%' : '—'}</div>
7457
- <div style="font-size:10px;color:#9ca3af">${nextTier ? 'tier ' + nextTier.tier : t('已达顶')}</div>
7774
+ <div style="font-size:11px;color:#6b7280">👥 ${t('直推')}</div>
7775
+ <div style="font-size:17px;font-weight:700;color:#0891b2;margin-top:2px">${Number(data.team?.l1 || 0)}</div>
7776
+ <div style="font-size:10px;color:#9ca3af">${t('')}</div>
7458
7777
  </div>
7459
7778
  </div>`
7460
7779
  }
@@ -7497,23 +7816,9 @@ async function renderPromoter(app) {
7497
7816
  ? `<span style="color:#16a34a">✅ ${t('已开通分享分润资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
7498
7817
  : `<span style="color:#d97706">⏳ ${t('分享分润待开通')}</span> · <span>${t('完成首笔购买即可')}</span>`}
7499
7818
  ${data.my_sponsor ? `<br>${t('邀请人')}: <strong>${escHtml(t(data.my_sponsor.name))}</strong>` : ''}
7500
- ${_pvAllowed && atomic.my_placement ? ` · ${t('挂靠')}: <strong>${escHtml(t(atomic.my_placement.name))}</strong> ${atomic.my_placement.side === 'left' ? '🔵' : '🟢'}` : ''}
7501
7819
  · ${t('所在地区')}: ${regionLabel(data.region || 'global')}
7502
7820
  </div>
7503
7821
 
7504
- ${!_pvAllowed ? '' : `
7505
- <details style="margin-top:4px">
7506
- <summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('自动放置设置')}</summary>
7507
- <div style="padding:8px 0">
7508
- <div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('新人通过你的推荐码注册后,系统自动安排积分树位置(无需选择左右)。')}</div>
7509
- <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动放置依据')}</div>
7510
- <select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
7511
- <option value="team_count">${t('推荐少的一边(默认)')}</option>
7512
- <option value="pv_count">${t('近 90 天积分少的一边')}</option>
7513
- </select>
7514
- <div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
7515
- </div>
7516
- </details>`}
7517
7822
  <div style="margin-top:12px;display:flex;justify-content:flex-end">
7518
7823
  <button class="btn btn-outline btn-sm" onclick="closeModal()">${t('关闭')}</button>
7519
7824
  </div>`
@@ -7719,47 +8024,24 @@ async function renderPromoter(app) {
7719
8024
  </div>
7720
8025
  </details>`
7721
8026
 
7722
- // ─── ⑥ WebAZ 发展奖(默认折叠;紧张 PV 地区精简版)───
7723
- // 合规:max_levels ≤ 1 的地区(GCC/越南/印尼/菲律宾 + 未审计地区)
7724
- // 只显示"累计推广 N 人"客观数字,全部金钱/tier/匹配话术隐藏
7725
- // 复用顶部 _kpiRestricted(同一含义,避免重复定义)
8027
+ // ─── ⑥ 参与记录(PV)───
7726
8028
  const _totalRecruits = Number(data.team?.l1 || 0) + Number(data.team?.l2 || 0) + Number(data.team?.l3 || 0)
7727
- // 最右侧地区显示(所有模式都加)
7728
8029
  const _userRegion = state.user?.region || 'global'
7729
8030
  const _regionChip = `<span style="font-size:11px;color:#6b7280;white-space:nowrap;font-weight:400">${regionLabel(_userRegion)}</span>`
7730
- // PV 双轨系统:只在 _pvAllowed(region_pv_enabled=1,2026-06-04 解耦,独立于 max_levels)才显示完整 tier/匹配/弱侧;
7731
- // 未开启 PV 的地区 → 走精简卡(分数仍后台计算累积,待 pv_enabled 开启/迁移后兑现)
7732
- const atomicSection = atomic.left_invite_url
7733
- ? (!_pvAllowed
8031
+ // 匹配奖励引擎已切除(#401):只展示中性参与记录(PV = 参与/贡献的记录,非收益、不可兑付、无权益)。
8032
+ const atomicSection = (_recordingOn
7734
8033
  ? `<div style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px">
7735
8034
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:8px">
7736
- <span style="font-size:14px;font-weight:600">🌟 ${t('WebAZ 发展奖')}</span>
7737
- <div style="display:flex;align-items:center;gap:8px">
7738
- <span style="font-size:11px;color:#9ca3af">${t('协议级荣誉')}</span>
7739
- ${_regionChip}
7740
- </div>
8035
+ <span style="font-size:14px;font-weight:600">📒 ${t('参与记录')}</span>
8036
+ ${_regionChip}
7741
8037
  </div>
7742
8038
  <div style="display:flex;align-items:baseline;gap:8px;margin-top:6px">
7743
- <span style="font-size:24px;font-weight:800;color:#7c3aed">${_totalRecruits}</span>
7744
- <span style="font-size:12px;color:#6b7280">${t('累计推广用户')}</span>
8039
+ <span style="font-size:24px;font-weight:800;color:#4338ca">${(leftPv + rightPv).toLocaleString()}</span>
8040
+ <span style="font-size:12px;color:#6b7280">${t('累计参与积分(PV)')}</span>
7745
8041
  </div>
7746
- <div style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('你所在地区的协议规则下,推广奖励以荣誉记录形式展示')}</div>
8042
+ <div style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('PV 是参与 / 贡献的记录,不是收益、不可兑付、不构成任何奖励权益。匹配奖励当前未启用。')}</div>
7747
8043
  </div>`
7748
- : `<details style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
7749
- <summary style="padding:12px;font-size:14px;font-weight:600;cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center;gap:8px">
7750
- <span>
7751
- 🌟 ${t('WebAZ 发展奖')}
7752
- <span style="font-weight:400;font-size:12px;color:#6b7280;margin-left:6px">
7753
- ${t('弱侧')} ${weak.toLocaleString()} PV · ${t('累计')} ${(atomic.score?.settled_waz || 0).toFixed(2)} WAZ
7754
- </span>
7755
- </span>
7756
- ${_regionChip}
7757
- </summary>
7758
- <div style="padding:0 12px 12px">
7759
- ${renderAtomicInner(atomic, leftPv, rightPv, weak, nextTier, nextProgress)}
7760
- </div>
7761
- </details>`)
7762
- : ''
8044
+ : '')
7763
8045
 
7764
8046
  // ─── ⑦ 实时流水(默认折叠)───
7765
8047
  const bulldozerEvents = (data.recent || []).map(r => ({
@@ -7767,12 +8049,7 @@ async function renderPromoter(app) {
7767
8049
  label: `🚜 L${r.level} ${t('佣金')} · ${escHtml(r.source_buyer_name || '—')}`,
7768
8050
  rate: r.rate,
7769
8051
  }))
7770
- const atomicEvents = (atomic.recent_binary || []).map(r => ({
7771
- kind: 'binary', ts: r.created_at, level: 0, amount: Number(r.waz_amount || 0),
7772
- label: `⚛ tier ${r.tier} · Score ${r.score}${r.settled_at ? '' : ' (' + t('待结') + ')'}`,
7773
- settled: !!r.settled_at,
7774
- }))
7775
- const merged = [...bulldozerEvents, ...atomicEvents]
8052
+ const merged = [...bulldozerEvents]
7776
8053
  .sort((a, b) => (b.ts || '').localeCompare(a.ts || ''))
7777
8054
  .slice(0, 30)
7778
8055
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
@@ -7936,87 +8213,6 @@ window.setBoughtKw = (kw) => {
7936
8213
  }, 200)
7937
8214
  }
7938
8215
 
7939
- // 积分匹配内部内容(折叠展开后渲染,从 renderAtomicSection 简化抽出)
7940
- function renderAtomicInner(a, leftPv, rightPv, weak, nextTier, nextProgress) {
7941
- const tiers = a.tier_config || []
7942
- const tierTable = tiers.map(x => `
7943
- <tr style="border-bottom:1px solid #f3f4f6">
7944
- <td style="padding:5px 8px;font-size:12px">tier ${x.tier}</td>
7945
- <td style="padding:5px 8px;font-size:12px;text-align:right">${Number(x.pv_threshold).toLocaleString()}</td>
7946
- <td style="padding:5px 8px;font-size:12px;text-align:right;font-weight:600">${x.score_per_hit}</td>
7947
- </tr>`).join('')
7948
-
7949
- // 本月匹配次数 + WAZ
7950
- const now = new Date()
7951
- const monthStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10)
7952
- const monthBinary = (a.recent_binary || []).filter(r => (r.created_at || '').slice(0, 10) >= monthStart)
7953
- const monthHits = monthBinary.length
7954
- const monthWaz = monthBinary.reduce((s, r) => s + Number(r.waz_amount || 0), 0)
7955
-
7956
- // 总 hits / 总下线(直挂左右子节点)
7957
- const score = a.score || {}
7958
- const totalHits = score.total_hits || 0
7959
-
7960
- const recentRows = (a.recent_binary || []).length
7961
- ? a.recent_binary.map(r => `
7962
- <div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:12px">
7963
- <div>tier ${r.tier} · Score <strong>${r.score}</strong> · ${r.settled_at ? `<span style="color:#16a34a">${t('已结')} ${Number(r.waz_amount).toFixed(2)}</span>` : `<span style="color:#d97706">${t('待分配')}</span>`}</div>
7964
- <div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
7965
- </div>`).join('')
7966
- : `<div style="text-align:center;color:#9ca3af;padding:10px;font-size:12px">${t('暂无匹配记录')}</div>`
7967
-
7968
- return `
7969
- <!-- 我的位置 -->
7970
- ${a.my_placement ? `
7971
- <div style="background:#f5f3ff;border:1px solid #e9d5ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
7972
- 📍 ${t('我挂位置')}: <strong>${escHtml(a.my_placement.name)}</strong> 的 ${a.my_placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'}
7973
- </div>` : `
7974
- <div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:8px 10px;font-size:12px;color:#9ca3af;margin-bottom:10px">
7975
- ${t('你还没加入任何上级的积分树(独立根节点)')}
7976
- </div>`}
7977
-
7978
- <!-- 左/右 PV -->
7979
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
7980
- <div style="text-align:center;background:#eff6ff;border-radius:6px;padding:10px">
7981
- <div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
7982
- <div style="font-size:18px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
7983
- <div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
7984
- </div>
7985
- <div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:10px">
7986
- <div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
7987
- <div style="font-size:18px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
7988
- <div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
7989
- </div>
7990
- </div>
7991
-
7992
- <!-- 本月匹配统计 + 总匹配 -->
7993
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;margin-bottom:10px">
7994
- <div style="text-align:center;background:#fef3c7;border-radius:6px;padding:8px 4px">
7995
- <div style="font-size:10px;color:#92400e">${t('本月匹配次数')}</div>
7996
- <div style="font-size:14px;font-weight:700;color:#78350f">${monthHits}</div>
7997
- </div>
7998
- <div style="text-align:center;background:#dcfce7;border-radius:6px;padding:8px 4px">
7999
- <div style="font-size:10px;color:#166534">${t('本月匹配 WAZ')}</div>
8000
- <div style="font-size:14px;font-weight:700;color:#14532d">${monthWaz.toFixed(2)}</div>
8001
- </div>
8002
- <div style="text-align:center;background:#eef2ff;border-radius:6px;padding:8px 4px">
8003
- <div style="font-size:10px;color:#4338ca">${t('累计匹配次数')}</div>
8004
- <div style="font-size:14px;font-weight:700;color:#3730a3">${totalHits}</div>
8005
- </div>
8006
- </div>
8007
-
8008
- ${renderBinaryTree(a.binary_tree)}
8009
-
8010
- <!-- pre-public de-MLM: 弱腿/对碰 tier 进度 + Score/次 档位表已下线;PV/位置仅为参与记录,非收益路径 -->
8011
- <div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
8012
- ${window._lang === 'en'
8013
- ? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
8014
- : 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8015
- </div>
8016
- <h4 style="font-size:12px;font-weight:600;margin:6px 0">📊 ${t('最近匹配')}</h4>
8017
- ${recentRows}`
8018
- }
8019
-
8020
8216
  // ─── 🎯 成长任务 UI(替换 insights,主线:分享达人养成)───
8021
8217
  function renderGrowthTasksSection(tasks, summary) {
8022
8218
  if (!tasks || tasks.length === 0) return ''
@@ -8151,200 +8347,6 @@ window.growthTaskAction = (action, id) => {
8151
8347
  }
8152
8348
  }
8153
8349
 
8154
- function renderAtomicSection(a) {
8155
- if (!a || !a.left_invite_url) return ''
8156
- const leftPv = Number(a.total_left_pv || 0)
8157
- const rightPv = Number(a.total_right_pv || 0)
8158
- const pair = Math.min(leftPv, rightPv)
8159
- const tiers = a.tier_config || []
8160
- // 下一档进度
8161
- const nextTier = tiers.find(t => t.pv_threshold > pair) || tiers[tiers.length - 1]
8162
- const nextProgress = nextTier ? Math.min(100, (pair / nextTier.pv_threshold) * 100) : 0
8163
-
8164
- const tierTable = tiers.map(t => `
8165
- <tr style="border-bottom:1px solid #f3f4f6">
8166
- <td style="padding:6px 8px;font-size:12px">tier ${t.tier}</td>
8167
- <td style="padding:6px 8px;font-size:12px;text-align:right">${Number(t.pv_threshold).toLocaleString()}</td>
8168
- <td style="padding:6px 8px;font-size:12px;text-align:right;font-weight:600">${t.score_per_hit}</td>
8169
- </tr>`).join('')
8170
-
8171
- const recentRows = (a.recent_binary || []).length
8172
- ? a.recent_binary.map(r => `
8173
- <div style="display:flex;justify-content:space-between;padding:6px 12px;border-bottom:1px solid #f3f4f6;font-size:12px">
8174
- <div>tier ${r.tier} · Score <strong>${r.score}</strong> · ${r.settled_at ? `<span style="color:#16a34a">已结 ${Number(r.waz_amount).toFixed(2)} WAZ</span>` : `<span style="color:#d97706">待分配</span>`}</div>
8175
- <div style="color:#9ca3af">${fmtTime(r.created_at)}</div>
8176
- </div>`).join('')
8177
- : `<div style="text-align:center;color:#9ca3af;padding:16px;font-size:12px">${t('暂无匹配记录')}</div>`
8178
-
8179
- return `
8180
- <h2 style="font-size:15px;font-weight:600;margin:24px 0 8px">⚛ ${t('积分 — 积分匹配')}</h2>
8181
-
8182
- <div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#dbeafe,#f0fdf4)">
8183
- <div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('我的推荐码')}</div>
8184
- <div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('复制你的推荐链接分享给新人;新人注册后由系统自动安排积分树位置(无需选择左右)。')}</div>
8185
- ${(() => {
8186
- // pre-public 去左右码:只用唯一的推荐码(/i/CODE,不带 -L/-R);缺失则提示不可用
8187
- const myCode = state.user?.permanent_code || null
8188
- const origin = location.origin
8189
- if (!myCode) return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
8190
- const link = `${origin}/i/${myCode}`
8191
- const esc = (s) => s.replace(/'/g, "\\'")
8192
- return `
8193
- <div onclick="copyRefLink('${esc(link)}')" style="cursor:pointer;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#e0e7ff'" onmouseout="this.style.background='#eef2ff'">
8194
- <div style="font-size:13px;font-weight:700;color:#4338ca;margin-bottom:6px">${t('推荐链接')}</div>
8195
- <div style="font-size:10px;color:#4338ca;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(link)}</div>
8196
- <div style="font-size:11px;color:#6366f1;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
8197
- </div>`
8198
- })()}
8199
- <details style="margin-top:10px">
8200
- <summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动放置依据')}</summary>
8201
- <div style="padding:8px 0">
8202
- <select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
8203
- <option value="team_count">${t('推荐少的一边(默认)')}</option>
8204
- <option value="pv_count">${t('近 90 天积分少的一边')}</option>
8205
- </select>
8206
- <div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
8207
- </div>
8208
- </details>
8209
- ${a.my_placement ? `<div style="font-size:11px;color:#6b7280;margin-top:8px">${t('我挂靠位置')}: <strong>${escHtml(a.my_placement.name)}</strong> 的 ${a.my_placement.side === 'left' ? '🔵 左区' : '🟢 右区'}</div>` : ''}
8210
- </div>
8211
-
8212
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
8213
- <div class="card" style="text-align:center;background:#eff6ff">
8214
- <div style="font-size:11px;color:#6b7280">🔵 ${t('左区 PV')}</div>
8215
- <div style="font-size:20px;font-weight:700;color:#1e40af">${leftPv.toLocaleString()}</div>
8216
- <div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.left_child ? escHtml(a.left_child.name) : '—'}</div>
8217
- </div>
8218
- <div class="card" style="text-align:center;background:#f0fdf4">
8219
- <div style="font-size:11px;color:#6b7280">🟢 ${t('右区 PV')}</div>
8220
- <div style="font-size:20px;font-weight:700;color:#15803d">${rightPv.toLocaleString()}</div>
8221
- <div style="font-size:10px;color:#9ca3af">${t('直挂')}: ${a.right_child ? escHtml(a.right_child.name) : '—'}</div>
8222
- </div>
8223
- </div>
8224
-
8225
- ${renderBinaryTree(a.binary_tree)}
8226
-
8227
- <!-- pre-public de-MLM: 弱腿/对碰 tier 进度已下线;PV/位置仅为参与记录,非收益路径 -->
8228
- <div class="card" style="margin-bottom:12px">
8229
- <div style="font-size:11px;color:#9ca3af;line-height:1.6">
8230
- ${window._lang === 'en'
8231
- ? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
8232
- : 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8233
- </div>
8234
- </div>
8235
-
8236
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
8237
- <div class="card" style="text-align:center">
8238
- <div style="font-size:11px;color:#6b7280">${t('待结算 Score')}</div>
8239
- <div style="font-size:18px;font-weight:700;color:#d97706">${(a.score?.pending_score || 0).toFixed(0)}</div>
8240
- </div>
8241
- <div class="card" style="text-align:center">
8242
- <div style="font-size:11px;color:#6b7280">${t('累计获 WAZ')}</div>
8243
- <div style="font-size:18px;font-weight:700;color:#059669">${(a.score?.settled_waz || 0).toFixed(2)}</div>
8244
- </div>
8245
- </div>
8246
-
8247
- <h3 style="font-size:13px;font-weight:600;margin:8px 0">📊 ${t('最近匹配')}</h3>
8248
- <div class="card" style="padding:0">
8249
- ${recentRows}
8250
- </div>`
8251
- }
8252
-
8253
- // P12: 三层积分树(你 + 左右 + 各自左右)
8254
- function renderBinaryTree(tree) {
8255
- if (!tree || !tree.me) return ''
8256
- const node = (n, bg, fg, label) => n
8257
- ? `<div style="padding:6px 4px;background:${bg};border-radius:6px;font-size:10px;line-height:1.3;color:${fg};text-align:center;min-height:42px;display:flex;flex-direction:column;justify-content:center;cursor:pointer" onclick="showNodePvModal('${n.id}')" title="${t('点击查看 PV KPI')}">
8258
- <div style="font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(n.name || '—')}</div>
8259
- <div style="font-size:9px;opacity:0.8">L${Math.round(n.lpv)}/R${Math.round(n.rpv)}</div>
8260
- </div>`
8261
- : `<div style="padding:6px 4px;background:#f9fafb;border-radius:6px;font-size:10px;color:#d1d5db;text-align:center;min-height:42px;display:flex;flex-direction:column;justify-content:center">${label || '—'}</div>`
8262
-
8263
- return `
8264
- <details style="margin-bottom:12px" open>
8265
- <summary style="font-size:12px;color:#6366f1;cursor:pointer;margin-bottom:6px">🌳 ${t('团队组织图(3 层)')} <span style="color:#9ca3af;font-size:10px">${t('点击节点查看 PV')}</span></summary>
8266
- <div style="padding:8px;background:#fff;border:1px solid #e5e7eb;border-radius:8px">
8267
- <!-- L0: 你 -->
8268
- <div style="margin-bottom:6px">
8269
- ${node(tree.me, '#e0e7ff', '#3730a3', '')}
8270
- </div>
8271
- <div style="text-align:center;color:#d1d5db;font-size:12px;margin:-2px 0">┌────┴────┐</div>
8272
- <!-- L1: 左 / 右 -->
8273
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:4px">
8274
- ${node(tree.left, '#dbeafe', '#1e40af', t('空左'))}
8275
- ${node(tree.right, '#dcfce7', '#15803d', t('空右'))}
8276
- </div>
8277
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;color:#d1d5db;font-size:11px;text-align:center;margin:-2px 0">
8278
- <span>┌──┴──┐</span><span>┌──┴──┐</span>
8279
- </div>
8280
- <!-- L2 -->
8281
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:4px">
8282
- ${node(tree.ll, '#eff6ff', '#1e40af', '·')}
8283
- ${node(tree.lr, '#eff6ff', '#1e40af', '·')}
8284
- ${node(tree.rl, '#f0fdf4', '#15803d', '·')}
8285
- ${node(tree.rr, '#f0fdf4', '#15803d', '·')}
8286
- </div>
8287
- <div style="font-size:10px;color:#9ca3af;margin-top:6px;text-align:center">${t('每节点显示 L=左累计 PV / R=右累计 PV · 点击节点看详情')}</div>
8288
- </div>
8289
- </details>`
8290
- }
8291
-
8292
- // 节点 PV KPI modal — 点组织图任一节点弹出
8293
- window.showNodePvModal = async (userId) => {
8294
- if (!userId) return
8295
- _openModal(`
8296
- <h2 style="font-size:16px;font-weight:600;margin-bottom:12px">📊 ${t('节点 PV 详情')}</h2>
8297
- <div id="pv-modal-body" style="text-align:center;padding:20px;color:#9ca3af;font-size:13px">${t('加载中...')}</div>
8298
- <div style="display:flex;gap:8px;margin-top:8px">
8299
- <button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('关闭')}</button>
8300
- </div>
8301
- `)
8302
- const data = await GET(`/users/${userId}/pv-summary`)
8303
- const body = document.getElementById('pv-modal-body')
8304
- if (!body) return
8305
- if (data?.error) {
8306
- body.innerHTML = alert$('error', data.error)
8307
- return
8308
- }
8309
- // pre-public de-MLM: 不再展示弱腿/对碰收益(weak-leg / pairing earnings)。PV / 位置仅为参与记录。
8310
- body.innerHTML = `
8311
- <div style="text-align:left">
8312
- <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6">
8313
- <div style="width:42px;height:42px;border-radius:50%;background:#eef2ff;display:flex;align-items:center;justify-content:center;font-size:20px">👤</div>
8314
- <div style="flex:1;min-width:0">
8315
- <div style="font-size:15px;font-weight:600">${escHtml(data.name)}</div>
8316
- <div style="font-size:11px;color:#9ca3af">${data.permanent_code || data.id} ${data.handle ? '· @' + data.handle : ''}</div>
8317
- </div>
8318
- </div>
8319
- ${data.placement ? `
8320
- <div style="background:#f5f3ff;border-radius:6px;padding:8px 10px;font-size:12px;color:#6b21a8;margin-bottom:10px">
8321
- 📍 ${t('挂位置')}: ${escHtml(data.placement.name || data.placement.id)} ${data.placement.side === 'left' ? '🔵 左侧' : '🟢 右侧'} · ${t('深度')} ${data.placement.depth}
8322
- </div>` : `<div style="font-size:11px;color:#9ca3af;margin-bottom:10px">${t('独立根节点(无上级)')}</div>`}
8323
-
8324
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
8325
- <div style="text-align:center;background:#eff6ff;border-radius:6px;padding:8px">
8326
- <div style="font-size:10px;color:#6b7280">🔵 ${t('左区 PV')}</div>
8327
- <div style="font-size:16px;font-weight:700;color:#1e40af">${Number(data.total_left_pv).toLocaleString()}</div>
8328
- <div style="font-size:9px;color:#9ca3af">${data.left_child ? escHtml(data.left_child.name) : t('空')}</div>
8329
- </div>
8330
- <div style="text-align:center;background:#f0fdf4;border-radius:6px;padding:8px">
8331
- <div style="font-size:10px;color:#6b7280">🟢 ${t('右区 PV')}</div>
8332
- <div style="font-size:16px;font-weight:700;color:#15803d">${Number(data.total_right_pv).toLocaleString()}</div>
8333
- <div style="font-size:9px;color:#9ca3af">${data.right_child ? escHtml(data.right_child.name) : t('空')}</div>
8334
- </div>
8335
- </div>
8336
-
8337
- <div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
8338
- ${window._lang === 'en'
8339
- ? 'PV pairing is pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
8340
- : 'PV 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8341
- </div>
8342
-
8343
- <a href="#u/${data.id}" onclick="closeModal()" style="display:block;text-align:center;font-size:12px;color:#4f46e5;text-decoration:none;padding:8px">→ ${t('查看 TA 的主页')}</a>
8344
- </div>
8345
- `
8346
- }
8347
-
8348
8350
  // 健壮复制:先 clipboard API,失败回退 execCommand(不依赖 focus/activation)
8349
8351
  // 任何 async 后调用都安全。window.copyText 暴露给 inline onclick handlers
8350
8352
  async function copyText(text) {
@@ -10433,6 +10435,8 @@ async function renderMyContributions(app) {
10433
10435
  const gid = await GET('/contribution-identity/github/me').catch(() => null)
10434
10436
  // F10 — 自动发现可认领的 GitHub 贡献(只读;失败优雅降级)
10435
10437
  const claimable = await GET('/contribution-identity/github/claimable').catch(() => null)
10438
+ // Contribution read-out V1 — 已归属到本账号的贡献事实(GitHub + 管理协调;只读;失败优雅降级)
10439
+ const cf = await GET('/contribution-facts/me').catch(() => null)
10436
10440
  const lang = window._lang === 'zh' ? 'zh' : 'en'
10437
10441
  const tier = p.tier || {}
10438
10442
  const k = p.kpi || {}
@@ -10470,6 +10474,7 @@ async function renderMyContributions(app) {
10470
10474
  ${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
10471
10475
  ${!p.passkey_anchor_present ? `<div class="card" style="padding:10px;margin-bottom:12px;border-left:3px solid #d97706"><div style="font-size:12px;color:#92400e">${t('未绑 Passkey:贡献可受理致谢,但需绑定真人锚点才记入建设信誉。')}</div></div>` : ''}
10472
10476
  ${ghClaimSectionHtml(gid, claimable, lang)}
10477
+ ${contributionFactsSectionHtml(cf, lang)}
10473
10478
  <div class="card" onclick="location.hash='#contribute/tasks'" style="padding:12px 14px;margin-bottom:12px;cursor:pointer;display:flex;align-items:center;gap:10px">
10474
10479
  <div style="font-size:20px;flex-shrink:0">📋</div>
10475
10480
  <div style="flex:1;min-width:0">
@@ -10510,6 +10515,56 @@ function ghClaimErrText(code, fallback) {
10510
10515
  return m[code] || fallback || t('操作失败')
10511
10516
  }
10512
10517
 
10518
+ // Contribution read-out V1 — 「贡献事实记录」只读区块。展示已归属到本账号的 contribution facts
10519
+ // (GitHub + 管理协调),按来源分组。这是事实与归属记录,不是奖励/付款/兑现权利。不暴露 admin audit detail。
10520
+ function contributionFactsSectionHtml(cf, lang) {
10521
+ const ok = cf && !cf.error
10522
+ const groups = ok ? (cf.groups || {}) : {}
10523
+ const gh = ok ? (groups.github || []) : []
10524
+ const ac = ok ? (groups.admin_coordination || []) : []
10525
+ const total = ok ? (cf.total || 0) : 0
10526
+ const notice = ok && cf.value_boundary ? (lang === 'zh' ? cf.value_boundary.notice_zh : cf.value_boundary.notice_en) : ''
10527
+ if (!ok) {
10528
+ return `<div class="card" style="padding:14px;margin-bottom:12px">
10529
+ <div style="font-size:13px;font-weight:600;margin-bottom:4px">📜 ${_qT('贡献事实记录', 'Contribution evidence')}</div>
10530
+ <div style="font-size:12px;color:#9ca3af">${_qT('暂不可用,稍后再试', 'Unavailable, try again later')}</div>
10531
+ </div>`
10532
+ }
10533
+ const statusLabel = (s) => ({ active: _qT('有效', 'active'), superseded: _qT('被替代', 'superseded'), reverted: _qT('已撤销', 'reverted'), void: _qT('作废', 'void'), forfeited: _qT('已没收', 'forfeited') }[String(s)] || escHtml(String(s)))
10534
+ const factRow = (f) => {
10535
+ const label = lang === 'zh' ? (f.display_source_label || '') : (f.display_source_label_en || f.display_source_label || '')
10536
+ const middle = (f.evidence_ref && f.evidence_ref.source_type) ? f.evidence_ref.source_type : (f.type || f.source || '')
10537
+ const date = String(f.occurred_at || '').slice(0, 10)
10538
+ return `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
10539
+ <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
10540
+ <span style="background:#eef2ff;color:#4338ca;border-radius:10px;padding:1px 8px;font-size:10px;font-weight:600">${escHtml(label)}</span>
10541
+ <b style="word-break:break-all">${escHtml(String(middle))}</b>
10542
+ ${date ? `<span style="color:#6b7280">· ${escHtml(date)}</span>` : ''}
10543
+ <span style="color:#9ca3af">· ${statusLabel(f.status)}</span>
10544
+ </div>
10545
+ <div style="color:#9ca3af;font-size:10px;margin-top:2px">fact <code>${escHtml(String(f.fact_id).slice(0, 18))}…</code></div>
10546
+ </div>`
10547
+ }
10548
+ const group = (titleZh, titleEn, items, emptyZh, emptyEn) => `
10549
+ <div style="margin-top:10px">
10550
+ <div style="font-size:12px;font-weight:600;color:#374151;margin-bottom:4px">${_qT(titleZh, titleEn)} <span style="color:#9ca3af;font-weight:400">(${items.length})</span></div>
10551
+ ${items.length ? items.map(factRow).join('') : `<div style="font-size:11px;color:#9ca3af">${_qT(emptyZh, emptyEn)}</div>`}
10552
+ </div>`
10553
+ return `
10554
+ <div class="card" style="padding:14px;margin-bottom:12px">
10555
+ <div style="font-size:13px;font-weight:600;margin-bottom:4px">📜 ${_qT('贡献事实记录', 'Contribution evidence')} <span style="color:#9ca3af;font-weight:400">· ${total}</span></div>
10556
+ <div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">⚠️ ${_qT('这里是贡献事实与归属记录,不是奖励、不是付款、不是兑现权利。', 'These are contribution facts and attribution records only — not a payment, and they confer no economic or redemption right.')}</div>
10557
+ ${total === 0 ? `<div style="font-size:12px;color:#9ca3af">${_qT('暂无已归属到你的贡献事实。', 'No contribution facts attributed to you yet.')}</div>` : `
10558
+ ${group('GitHub', 'GitHub', gh, '暂无 GitHub 贡献事实', 'No GitHub facts')}
10559
+ ${group('管理协调', 'Admin coordination', ac, '暂无管理协调贡献事实', 'No admin-coordination facts')}
10560
+ <div style="margin-top:10px">
10561
+ <div style="font-size:12px;font-weight:600;color:#9ca3af;margin-bottom:4px">${_qT('Agent 授权执行', 'Agent-authorized execution')} <span style="font-weight:400">(${_qT('未来', 'future')})</span></div>
10562
+ <div style="font-size:11px;color:#9ca3af">${_qT('暂无', 'None yet')}</div>
10563
+ </div>`}
10564
+ ${notice ? `<div style="font-size:10px;color:#9ca3af;background:#f4f4f5;border-radius:6px;padding:6px 8px;margin-top:10px">🔒 ${escHtml(notice)}</div>` : ''}
10565
+ </div>`
10566
+ }
10567
+
10513
10568
  function ghClaimSectionHtml(gid, claimable, lang) {
10514
10569
  const ok = gid && !gid.error
10515
10570
  const bindings = ok ? (gid.bindings || []) : []
@@ -12480,19 +12535,6 @@ window.switchLoginTab = (tab) => {
12480
12535
  async function checkRegGate() {
12481
12536
  try {
12482
12537
  const f = await GET('/system-flags')
12483
- // 邀请码轮询按钮开关 — 默认禁用,admin 开启后变可用
12484
- const btn = document.getElementById('btn-fetch-ref')
12485
- if (btn) {
12486
- if (f?.invite_rotation_enabled) {
12487
- btn.disabled = false
12488
- btn.title = t('点击向系统申领一个邀请码')
12489
- btn.style.cssText = 'white-space:nowrap;padding:0 12px;background:#4f46e5;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer'
12490
- } else {
12491
- btn.disabled = true
12492
- btn.title = t('该功能默认关闭,由管理员开启后可用')
12493
- btn.style.cssText = 'white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed'
12494
- }
12495
- }
12496
12538
  if (!f?.require_ref_to_register) return
12497
12539
  const hint = readShareHint()
12498
12540
  const el = document.getElementById('reg-gate-hint')
@@ -12510,14 +12552,6 @@ async function checkRegGate() {
12510
12552
  } catch {}
12511
12553
  }
12512
12554
 
12513
- window.doFetchInviteCode = async () => {
12514
- const r = await POST('/invite/rotate', {})
12515
- if (r.error) { alert(r.error); return }
12516
- // 仅自动填入推荐码 — 不展示推荐人信息(轮询是被动分配,非主动分享)
12517
- const inp = document.getElementById('inp-sponsor')
12518
- if (inp) inp.value = r.code
12519
- }
12520
-
12521
12555
  window.doLogin = async () => {
12522
12556
  const key = document.getElementById('inp-key').value.trim()
12523
12557
  if (!key) return showMsg('error', t('请粘贴 api_key'))
@@ -14548,13 +14582,13 @@ async function renderUserProfile(app, userId) {
14548
14582
 
14549
14583
  <div style="margin-bottom:14px">
14550
14584
  <div style="font-size:13px;color:#374151;margin-bottom:6px">${t('一句话简介')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 120 字')})</span></div>
14551
- <input class="form-control" id="bio-inp" placeholder="${t('例如:新加坡教育认证专家,分享真实测评')}" style="font-size:13px" value="${escHtml(data.bio || '')}" maxlength="120">
14585
+ <input class="form-control" id="bio-inp" placeholder="${t('例如:一句话介绍你自己')}" style="font-size:13px" value="${escHtml(data.bio || '')}" maxlength="120">
14552
14586
  </div>
14553
14587
 
14554
14588
  <div style="margin-bottom:14px">
14555
14589
  <div style="font-size:13px;color:#374151;margin-bottom:6px">🔍 ${t('流量口令')} <span style="color:#9ca3af;font-size:11px">(${t('≤ 40 字,字母/数字/汉字/-_.')})</span></div>
14556
14590
  <div style="display:flex;gap:8px">
14557
- <input class="form-control" id="anchor-inp" placeholder="${t('例如:思翔教育007')}" style="font-size:13px;flex:1" value="${escHtml(data.search_anchor || '')}" maxlength="40">
14591
+ <input class="form-control" id="anchor-inp" placeholder="${t('例如:好记的字母或数字组合')}" style="font-size:13px;flex:1" value="${escHtml(data.search_anchor || '')}" maxlength="40">
14558
14592
  <button class="btn btn-primary btn-sm" style="white-space:nowrap" onclick="saveSocialProfile()">${t('保存')}</button>
14559
14593
  </div>
14560
14594
  <p style="font-size:11px;color:#9ca3af;margin-top:4px">${t('在 TikTok / 小红书 口播这个口令,粉丝在 WebAZ 搜它就能找到你')}</p>
@@ -15237,7 +15271,7 @@ const AI_TOOLS = [
15237
15271
  },
15238
15272
  {
15239
15273
  name: 'search_by_anchor',
15240
- description: '按创作者"流量口令"查找评测内容(如 "思翔教育007")。返回外链评测(YouTube/TikTok/etc)+ 原生 P2P 评测。',
15274
+ description: '按创作者"流量口令"查找评测内容(如某创作者的口令)。返回外链评测(YouTube/TikTok/etc)+ 原生 P2P 评测。',
15241
15275
  input_schema: {
15242
15276
  type: 'object',
15243
15277
  properties: { anchor: { type: 'string', description: '创作者的口令字符串' } },
@@ -15789,7 +15823,7 @@ function renderAIMessages(messages) {
15789
15823
  ${t('试试问:')}<br>
15790
15824
  • ${t('"帮我找适合送 60 岁妈妈的礼物 ≤ 500 元"')}<br>
15791
15825
  • ${t('"附近最近有人买什么?"')}<br>
15792
- • ${t('"思翔教育007 这个口令的创作者发了啥?"')}
15826
+ • ${t('"某个口令的创作者发了啥?"')}
15793
15827
  </div>
15794
15828
  <a href="#ai-demo" style="display:inline-block;font-size:12px;color:#007aff;text-decoration:none;background:#eff6ff;border:0.5px solid #bfdbfe;padding:6px 14px;border-radius:99px">🎬 ${t('看看预设演示 →')}</a>
15795
15829
  </div>`
@@ -17947,9 +17981,11 @@ async function renderBuyPage(app, productId) {
17947
17981
  </button>`
17948
17982
  // 三柱 chip — 跳对应区域(在同页内 scrollIntoView)
17949
17983
  const reviewChip = chip('📝', t('评测'), '#7c3aed', '#f3e8ff', `document.getElementById('reviews-block-${p.id}')?.scrollIntoView({behavior:'smooth',block:'start'})`, reviewsCount > 0 ? reviewsCount : null)
17984
+ // 发起验证只对可发起者显示(登录且非本商品卖家);点击展开商品声明区的现成表单(替代旧的死路由 #claim-task/new)
17985
+ const _canClaim = !!state.user && state.user.id !== p.seller_id
17950
17986
  const claimChip = claimsResolved + claimsOpen > 0
17951
17987
  ? chip('✓', t('已验证'), '#059669', '#d1fae5', `document.getElementById('claims-block-${p.id}')?.scrollIntoView({behavior:'smooth',block:'start'})`, claimsResolved)
17952
- : chip('+', t('发起验证'), '#6366f1', '#eef2ff', `navigate('#claim-task/new?product=${p.id}')`)
17988
+ : (_canClaim ? chip('+', t('发起验证'), '#6366f1', '#eef2ff', `openProductClaimForm('${p.id}')`) : '')
17953
17989
  const disputeChip = sellerDisputes > 0
17954
17990
  ? chip('⚖', t('仲裁'), sellerOpenDisputes > 0 ? '#dc2626' : '#92400e', sellerOpenDisputes > 0 ? '#fee2e2' : '#fef3c7', `navigate('#shop/${p.seller_id}?tab=disputes')`, sellerDisputes)
17955
17991
  : ''
@@ -18370,7 +18406,6 @@ async function loadDisputeCaseDetail(caseId, container) {
18370
18406
  if (cm.role === 'verifier' || cm.role === 'arbitrator') tags.push('<span style="background:#ede9fe;color:#5b21b6;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">👨‍⚖️ ' + (cm.role === 'arbitrator' ? t('仲裁员') : t('验证员')) + '</span>')
18371
18407
  else if (cm.role === 'staff') tags.push('<span style="background:#ede9fe;color:#5b21b6;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">👨‍⚖️ ' + t('平台角色') + '</span>')
18372
18408
  if (tags.length === 0) tags.push('<span style="background:#f3f4f6;color:#6b7280;font-size:9px;padding:1px 6px;border-radius:99px">👀 ' + t('围观') + '</span>')
18373
- if (Number(cm.lifetime_score) >= 1000) tags.push('<span style="background:#fff7ed;color:#9a3412;font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600">⭐ ' + t('资深') + '</span>')
18374
18409
  // 身份显示:公开 → @handle 可点跳主页 ; 脱敏 → 匿名用户 灰字不可点
18375
18410
  const isAnon = !!cm.anonymous
18376
18411
  const nameHtml = isAnon
@@ -18915,6 +18950,19 @@ window.voteProductClaim = async (claimId, vote) => {
18915
18950
  setTimeout(() => renderClaimVerify(document.getElementById('app')), 1500)
18916
18951
  }
18917
18952
 
18953
+ // "+发起验证" chip → open the existing product-claim form in the claims block (scroll + expand collapsed
18954
+ // <details> + focus the target select). Replaces the dead #claim-task/new route. If the form isn't present
18955
+ // (not eligible), the scroll is a graceful no-op.
18956
+ window.openProductClaimForm = (productId) => {
18957
+ const block = document.getElementById(`claims-block-${productId}`)
18958
+ if (block) {
18959
+ block.scrollIntoView({ behavior: 'smooth', block: 'start' })
18960
+ block.querySelectorAll('details').forEach(d => { d.open = true })
18961
+ }
18962
+ const sel = document.getElementById(`pcl-target-${productId}`)
18963
+ if (sel) setTimeout(() => sel.focus(), 350)
18964
+ }
18965
+
18918
18966
  window.submitProductClaim = async (productId) => {
18919
18967
  const target = document.getElementById(`pcl-target-${productId}`)?.value
18920
18968
  const text = document.getElementById(`pcl-text-${productId}`)?.value?.trim()
@@ -25980,7 +26028,7 @@ async function renderWallet(app) {
25980
26028
  if (!state.user) { renderLogin(); return }
25981
26029
  app.innerHTML = shell(loading$(), 'wallet')
25982
26030
  const userRegion = state.user?.region || 'global'
25983
- const [wallet, income, deposits, withdrawals, whitelistRes, trust, tokenomics, rateRes, regionPmRes] = await Promise.all([
26031
+ const [wallet, income, deposits, withdrawals, whitelistRes, trust, rateRes, regionPmRes] = await Promise.all([
25984
26032
  // 2026-06-01 fix(BUG-PWA-WALLET): 加 catch 防 #wallet 加载死循环
25985
26033
  // 原 GET('/wallet') 无 fallback,一旦该 endpoint 错(网络 / 401 / 后端 500),
25986
26034
  // 整个 Promise.all reject,renderWallet 早退,"加载中..." 永驻
@@ -25990,7 +26038,6 @@ async function renderWallet(app) {
25990
26038
  GET('/wallet/withdrawals').catch(() => []),
25991
26039
  GET('/wallet/whitelist').catch(() => ({ whitelist: [] })),
25992
26040
  GET('/agents/me/reputation').catch(() => null),
25993
- GET('/tokenomics/status').catch(() => null),
25994
26041
  GET('/wallet/rate').catch(() => null),
25995
26042
  GET('/payment-methods/for-region?region=' + encodeURIComponent(userRegion)).catch(() => ({ items: [] })),
25996
26043
  ])
@@ -26029,7 +26076,7 @@ async function renderWallet(app) {
26029
26076
  const earned = Number(wallet.earned || 0)
26030
26077
  const totalAssets = balance + escrowed + staked
26031
26078
 
26032
- const inc = income || { commissions: { l1:{count:0,total:0}, l2:{count:0,total:0}, l3:{count:0,total:0} }, binary: { settled_count:0, settled_waz:0, pending_score:0 }, sales: { count:0, total:0 }, total_income: 0 }
26079
+ const inc = income || { commissions: { l1:{count:0,total:0}, l2:{count:0,total:0}, l3:{count:0,total:0} }, sales: { count:0, total:0 }, total_income: 0 }
26033
26080
  const dep = (deposits || []).slice(0, 5)
26034
26081
  const wdr = (withdrawals || []).slice(0, 5)
26035
26082
  const pendingWdr = wdr.filter(w => w.status === 'pending')
@@ -26050,48 +26097,11 @@ async function renderWallet(app) {
26050
26097
  ? `<button class="btn btn-outline btn-sm" style="font-size:10px;padding:2px 8px;color:#dc2626;border-color:#fecaca;margin-left:6px" onclick="cancelWithdrawal('${w.id}')">${t('取消')}</button>`
26051
26098
  : ''
26052
26099
 
26053
- // V3 用户等级 + 分配率卡
26054
- const userLevel = state.user?.user_level || { level: 0, name: t('游客'), nextThreshold: 1 }
26055
- const lifetimeScore = Number(state.user?.lifetime_score || 0)
26056
- const distCap = Number(tokenomics?.distribution_cap ?? 1.0)
26057
- const healthLevel = tokenomics?.health_level || 'cold_start'
26058
- const pausedRecent = tokenomics?.paused_recent
26059
- // P2 #7:R11 硬停 banner(仅在最近 7 天触发过时显示)
26060
- const pausedBanner = pausedRecent ? `
26061
- <div class="card" style="margin-bottom:12px;background:#fef3c7;border-color:#fde68a;padding:10px 14px">
26062
- <div style="font-size:13px;color:#92400e;font-weight:600;margin-bottom:2px">⚠️ ${t('结算暂停保护中')}</div>
26063
- <div style="font-size:11px;color:#78350f">${t('基金池水位保护已触发,本期 pending 奖金已保留,将在水位恢复后自动结算。')}</div>
26064
- </div>` : ''
26065
- const healthColor = {
26066
- healthy: { bg: '#dcfce7', fg: '#166534', label: t('健康') },
26067
- normal: { bg: '#dbeafe', fg: '#1e40af', label: t('正常') },
26068
- strained: { bg: '#fef3c7', fg: '#92400e', label: t('紧张') },
26069
- critical: { bg: '#fee2e2', fg: '#991b1b', label: t('告急') },
26070
- cold_start: { bg: '#f3f4f6', fg: '#6b7280', label: t('启动期') },
26071
- }[healthLevel] || { bg: '#f3f4f6', fg: '#6b7280', label: '—' }
26072
- const levelCard = `
26073
- <div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#fff7ed 0%,#fef3c7 100%);border-color:#fde68a">
26074
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
26075
- <div>
26076
- <div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">🏅 ${t('我的等级')}</div>
26077
- <div style="font-size:18px;font-weight:800;color:#78350f">L${userLevel.level} ${escHtml(userLevel.name)}</div>
26078
- <div style="font-size:10px;color:#a16207;margin-top:2px">${t('累计')}:¥${lifetimeScore.toFixed(0)}${userLevel.nextThreshold ? ` · ${t('下一档')} ¥${Number(userLevel.nextThreshold).toLocaleString()}` : ''}</div>
26079
- </div>
26080
- <div>
26081
- <div style="font-size:11px;color:#92400e;font-weight:600;margin-bottom:4px">💎 ${t('本期分配率')}</div>
26082
- <div style="font-size:18px;font-weight:800;color:#78350f">${(distCap * 100).toFixed(0)}%</div>
26083
- <div style="font-size:10px;margin-top:2px"><span style="background:${healthColor.bg};color:${healthColor.fg};padding:1px 6px;border-radius:99px;font-weight:600">${healthColor.label}</span> ${t('池子健康度')}</div>
26084
- </div>
26085
- </div>
26086
- </div>`
26100
+ // 奖励等级 / 分配率 / 池子健康度卡已移除(匹配奖励引擎已切除 #401 / #PR-D);钱包只展示中性资产 + 参与记录。
26087
26101
 
26088
26102
  app.innerHTML = shell(`
26089
26103
  <h1 class="page-title">💰 ${t('我的钱包')}</h1>
26090
26104
 
26091
- ${pausedBanner}
26092
-
26093
- ${levelCard}
26094
-
26095
26105
  ${renderTrustCard(trust)}
26096
26106
 
26097
26107
  <!-- ① 资产总览 -->
@@ -26116,7 +26126,7 @@ async function renderWallet(app) {
26116
26126
 
26117
26127
  <!-- 卖家收入分类速览(仅 seller 显示)- 2026-05-24 匹配列只在 PV 允许地区显示 -->
26118
26128
  ${state.user?.role === 'seller' ? (() => {
26119
- const _pvOK = Number(state.user?.region_pv_enabled ?? 0) === 1 // 2026-06-04 解耦:PV 列读 pv_enabled,不再绑 max≥3
26129
+ const _pvOK = inc.gates?.matching_rewards_active === true // Category C:匹配已结列读奖励闸门(默认关),不再读 region_pv_enabled
26120
26130
  const cols = _pvOK ? 4 : 3
26121
26131
  return `
26122
26132
  <div class="card" style="margin-bottom:12px;padding:14px">
@@ -26132,11 +26142,6 @@ async function renderWallet(app) {
26132
26142
  <div style="font-size:14px;font-weight:700;color:#059669;margin-top:2px">${((inc.commissions?.l1?.total||0)+(inc.commissions?.l2?.total||0)+(inc.commissions?.l3?.total||0)).toFixed(2)}</div>
26133
26143
  <div style="font-size:9px;color:#9ca3af">${((inc.commissions?.l1?.count||0)+(inc.commissions?.l2?.count||0)+(inc.commissions?.l3?.count||0))} ${t('单')}</div>
26134
26144
  </div>
26135
- ${_pvOK ? `<div style="background:#fefce8;border-radius:6px;padding:8px">
26136
- <div style="font-size:9px;color:#854d0e">⚛ ${t('匹配已结')}</div>
26137
- <div style="font-size:14px;font-weight:700;color:#a16207;margin-top:2px">${(inc.binary?.settled_waz||0).toFixed(2)}</div>
26138
- <div style="font-size:9px;color:#9ca3af">${inc.binary?.settled_count||0} ${t('次')}</div>
26139
- </div>` : ''}
26140
26145
  <div style="background:#fef3c7;border-radius:6px;padding:8px">
26141
26146
  <div style="font-size:9px;color:#92400e">⏳ ${t('待结算')}</div>
26142
26147
  <div style="font-size:14px;font-weight:700;color:#d97706;margin-top:2px">${escrowed.toFixed(2)}</div>
@@ -26167,11 +26172,6 @@ async function renderWallet(app) {
26167
26172
  <div style="text-align:center;background:#ecfeff;border-radius:6px;padding:10px"><div style="font-size:10px;color:#155e75">L2</div><div style="font-size:15px;font-weight:700;color:#0891b2">${inc.commissions.l2.total.toFixed(2)}</div><div style="font-size:9px;color:#9ca3af">${inc.commissions.l2.count} ${t('单')}</div></div>
26168
26173
  </div>`
26169
26174
  })()}
26170
- ${getMaxLevels() >= 3 ? `
26171
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
26172
- <div style="background:#fefce8;border-radius:6px;padding:10px"><div style="font-size:10px;color:#854d0e">⚛ ${t('积分已结算')}</div><div style="font-size:15px;font-weight:700;color:#a16207">${inc.binary.settled_waz.toFixed(2)} WAZ</div><div style="font-size:9px;color:#9ca3af">${inc.binary.settled_count} ${t('次')}</div></div>
26173
- <div style="background:#fef3c7;border-radius:6px;padding:10px"><div style="font-size:10px;color:#92400e">⏳ ${t('待结 Score')}</div><div style="font-size:15px;font-weight:700;color:#d97706">${inc.binary.pending_score.toFixed(1)}</div><div style="font-size:9px;color:#9ca3af">${t('每月结算')}</div></div>
26174
- </div>` : ''}
26175
26175
  ${inc.sales.count > 0 ? `
26176
26176
  <div style="background:#f0fdf4;border-radius:6px;padding:10px"><div style="font-size:10px;color:#166534">🏪 ${t('销售收入')}</div><div style="font-size:15px;font-weight:700;color:#15803d">${inc.sales.total.toFixed(2)} WAZ</div><div style="font-size:9px;color:#9ca3af">${inc.sales.count} ${t('单')}</div></div>
26177
26177
  ` : ''}
@@ -27426,7 +27426,8 @@ window.setClaimVerifyTab = (k) => {
27426
27426
  }
27427
27427
 
27428
27428
  async function renderClaimTaskDetail(app, taskId) {
27429
- if (!taskId) { navigate('#verify'); return }
27429
+ // 'new' 是历史死路由(无创建处理器);商品声明验证从商品页声明区的现成表单发起 回验证中心,避免报错页
27430
+ if (!taskId || taskId === 'new') { navigate('#verify'); return }
27430
27431
  if (!state.user) { renderLogin(); return }
27431
27432
  app.innerHTML = shell(loading$(), 'verify')
27432
27433
  const data = await GET(`/claim-tasks/${taskId}`)
@@ -31784,15 +31785,11 @@ async function renderAdminProtocol(app) {
31784
31785
  <!-- 协议金库摘要 -->
31785
31786
  <div class="card" style="margin-bottom:14px;background:linear-gradient(135deg,#eef2ff,#faf5ff);border-color:#c7d2fe;padding:16px">
31786
31787
  <div style="font-size:11px;color:#3730a3;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:10px">💎 ${t('协议金库摘要')}</div>
31787
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;font-size:12px">
31788
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;font-size:12px">
31788
31789
  <div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
31789
31790
  <div style="font-size:18px;font-weight:800;color:#4338ca">${Number(tk.pool_balance || 0).toFixed(2)}</div>
31790
31791
  <div style="color:#6b7280;margin-top:2px">${t('全球基金池')} WAZ</div>
31791
31792
  </div>
31792
- <div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
31793
- <div style="font-size:18px;font-weight:800;color:#7c3aed">${Number(tk.management_bonus || 0).toFixed(2)}</div>
31794
- <div style="color:#6b7280;margin-top:2px">${t('管理津贴池')} WAZ</div>
31795
- </div>
31796
31793
  <div style="background:rgba(255,255,255,0.7);border-radius:8px;padding:10px;text-align:center">
31797
31794
  <div style="font-size:18px;font-weight:800;color:#dc2626">${Number(fund.balance || 0).toFixed(2)}</div>
31798
31795
  <div style="color:#6b7280;margin-top:2px">${t('慈善基金')} WAZ</div>
@@ -31807,6 +31804,9 @@ async function renderAdminProtocol(app) {
31807
31804
  ${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
31808
31805
  ${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
31809
31806
  ${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 → 转正式任务'), '#admin/task-proposals')}
31807
+ ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🎟️', t('建任务额度审核'), t('非根管理员的建任务扩容申请;批准 = 限时计数授权(仅 root)'), '#admin/quota-requests') : ''}
31808
+ ${adminLinkCard('🔗', t('关联个人贡献账号'), t('把本管理席位的协调贡献归属到你的真实个人账号(需对方确认 + root 审批)'), '#me/operator-claims')}
31809
+ ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🪪', t('操作席位关联审批'), t('管理席位→个人贡献账号的关联申请;确认 + 审批 / 撤销(仅 root)'), '#admin/operator-claims') : ''}
31810
31810
  </div>
31811
31811
  `, 'admin-protocol')
31812
31812
  }