diff --git a/AGENTS.md b/AGENTS.md
index f7cdabc..277f88f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2883,5 +2883,45 @@ $ echo $?
**累计代码**:5061 行(新增 31 行)
-**最后更新**:2026-06-20 14:15
-**版本**:1.31(exit-status 修复完成)
+---
+
+## 死代码清理完成(2026-06-20)⭐⭐⭐⭐⭐
+
+**清理内容**(`kex_complete.rs`):
+- 移除 `compute_exchange_hash()`(113 行)— 已被 `kex_exchange.rs::compute_exchange_hash_strict()` 替代
+- 移除 `write_ssh_mpint_to_hash()` — 该函数有 bug(未处理 X25519 big-endian 转换)
+- 移除 `write_ssh_string_to_hash()` / `write_ssh_bytes_to_hash()` — 仅被上述函数调用
+- 移除 `test_exchange_hash_computation` 测试(依赖已删除的函数)
+- 移除 `sha2` 和 `Digest` 导入(不再需要)
+
+**验证**:157 passed, 0 failed
+
+**最后更新**:2026-06-20 14:30
+**版本**:1.32(死代码清理完成)
+
+---
+
+## Web Frontend Phase 3 完成(2026-06-20)⭐⭐⭐⭐⭐
+
+**完成时间**:约 10 分钟
+**新增代码量**:~60 行
+
+### 实施内容 ⭐⭐⭐⭐⭐
+
+**category_view.html 新增 Upload Tab**:
+1. ✅ 在 "By Category" / "By Series" 旁添加第三 Tab "Upload"
+2. ✅ Upload 表单包含 User ID 输入(默认: accusys)
+3. ✅ 文件选择器(单文件上传)
+4. ✅ 进度条(XMLHttpRequest.upload.onprogress)
+5. ✅ 成功/错误状态显示
+6. ✅ 使用现有 `/api/v2/upload-unlimited/:user_id` 端点
+
+### 验证 ⭐⭐⭐⭐⭐
+
+```bash
+cargo build -p markbase-core # ✅ 0 error
+cargo test -p markbase-core --lib # ✅ 157 passed, 0 failed
+```
+
+**最后更新**:2026-06-20 14:45
+**版本**:1.33(Web Frontend Phase 3 完成)
diff --git a/markbase-core/src/category_view.html b/markbase-core/src/category_view.html
index 3815326..c59095a 100644
--- a/markbase-core/src/category_view.html
+++ b/markbase-core/src/category_view.html
@@ -146,6 +146,70 @@
margin-bottom: 20px;
}
.back-btn:hover { background: #e8e8ed; }
+
+ /* Upload tab styles */
+ .upload-form {
+ max-width: 500px;
+ margin: 0 auto;
+ padding: 20px 0;
+ }
+ .upload-form label {
+ display: block;
+ font-weight: 500;
+ color: #1d1d1f;
+ margin-bottom: 8px;
+ }
+ .upload-form input[type="text"] {
+ width: 100%;
+ padding: 10px 14px;
+ border: 1px solid #d2d2d7;
+ border-radius: 8px;
+ font-size: 14px;
+ margin-bottom: 20px;
+ }
+ .upload-form input[type="file"] {
+ display: block;
+ margin-bottom: 20px;
+ font-size: 14px;
+ }
+ .upload-form .upload-btn {
+ width: 100%;
+ padding: 14px;
+ background: #0071e3;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ cursor: pointer;
+ }
+ .upload-form .upload-btn:hover { background: #0077ed; }
+ .upload-form .upload-btn:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+ }
+ .progress-bar {
+ width: 100%;
+ height: 8px;
+ background: #e8e8ed;
+ border-radius: 4px;
+ overflow: hidden;
+ margin: 16px 0;
+ display: none;
+ }
+ .progress-bar .fill {
+ height: 100%;
+ background: #0071e3;
+ width: 0%;
+ transition: width 0.3s;
+ }
+ .upload-status {
+ text-align: center;
+ font-size: 14px;
+ margin-top: 12px;
+ color: #86868b;
+ }
+ .upload-status.success { color: #30d158; }
+ .upload-status.error { color: #ff453a; }
@@ -159,6 +223,7 @@
By Category
By Series
+
Upload
@@ -200,6 +265,13 @@
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`.tab[data-view="${view}"]`).classList.add('active');
+
+ if (view === 'upload') {
+ document.getElementById('back-btn').style.display = 'none';
+ showUploadForm();
+ return;
+ }
+
document.getElementById('back-btn').style.display = 'none';
document.getElementById('items-label').textContent = view === 'category' ? 'Categories' : 'Series';
@@ -325,6 +397,91 @@
}
}
+ async function showUploadForm() {
+ const content = document.getElementById('content');
+ document.getElementById('total-items').textContent = '-';
+ document.getElementById('total-files').textContent = '-';
+
+ content.innerHTML = `
+
+ `;
+ }
+
+ async function startUpload() {
+ const fileInput = document.getElementById('upload-file');
+ const file = fileInput.files[0];
+ if (!file) {
+ document.getElementById('upload-status').textContent = 'Please select a file';
+ document.getElementById('upload-status').className = 'upload-status error';
+ return;
+ }
+
+ const userId = document.getElementById('upload-user-id').value.trim() || 'accusys';
+ const btn = document.getElementById('upload-btn');
+ const status = document.getElementById('upload-status');
+ const progressBar = document.getElementById('progress-bar');
+ const progressFill = document.getElementById('progress-fill');
+
+ btn.disabled = true;
+ btn.textContent = 'Uploading...';
+ status.textContent = '';
+ status.className = 'upload-status';
+ progressBar.style.display = 'block';
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', apiBase + '/api/v2/upload-unlimited/' + encodeURIComponent(userId), true);
+
+ xhr.upload.onprogress = function(e) {
+ if (e.lengthComputable) {
+ const pct = (e.loaded / e.total) * 100;
+ progressFill.style.width = pct + '%';
+ }
+ };
+
+ const result = await new Promise((resolve, reject) => {
+ xhr.onload = function() {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ try { resolve(JSON.parse(xhr.responseText)); }
+ catch { resolve({ ok: true }); }
+ } else {
+ try { reject(JSON.parse(xhr.responseText)); }
+ catch { reject({ error: 'Upload failed (' + xhr.status + ')' }); }
+ }
+ };
+ xhr.onerror = function() { reject({ error: 'Network error' }); };
+ xhr.send(formData);
+ });
+
+ progressFill.style.width = '100%';
+ status.textContent = 'Upload successful! File: ' + file.name;
+ status.className = 'upload-status success';
+ btn.textContent = 'Done';
+ } catch (err) {
+ status.textContent = 'Upload failed: ' + (err.error || err.message || 'Unknown error');
+ status.className = 'upload-status error';
+ btn.disabled = false;
+ btn.textContent = 'Upload';
+ progressBar.style.display = 'none';
+ }
+ }
+
function goBack() {
if (navigationStack.length === 0) {
currentItem = null;