Browse Source

fix:records.html

tanjingyu 1 week ago
parent
commit
628d3d4180
3 changed files with 204 additions and 113 deletions
  1. 29 22
      app/services/storage_service.py
  2. 146 91
      app/static/records.html
  3. 29 0
      reaggregate_records.py

+ 29 - 22
app/services/storage_service.py

@@ -128,32 +128,39 @@ class StorageService:
             else:
                 # Treat 'output' or None as output by default for rendering purposes
                 groups[group_key]["outputs"].append(file_data)
-        # 3. Insert aggregated records
+        # 3. Insert aggregated records (One record per output file)
         for group_key, data in groups.items():
-            # Calculate a deterministic content_hash for this group of files
-            # Combine all SHA values, sort them to ensure same set of files results in same hash
-            all_shas = [f["file_sha"] for f in data["inputs"]] + [f["file_sha"] for f in data["outputs"]]
-            all_shas.sort()
-            combined_string = "|".join(all_shas)
-            content_hash = hashlib.sha256(combined_string.encode('utf-8')).hexdigest()
+            inputs = data["inputs"]
+            outputs = data["outputs"]
+            
+            # If there are no outputs, still create one record for the inputs
+            # If there are outputs, create one record for EACH output
+            output_groups = [[o] for o in outputs] if outputs else [[]]
+            
+            for out_list in output_groups:
+                # Calculate a deterministic content_hash for this combination
+                all_shas = [f["file_sha"] for f in inputs] + [f["file_sha"] for f in out_list]
+                all_shas.sort()
+                combined_string = "|".join(all_shas)
+                content_hash = hashlib.sha256(combined_string.encode('utf-8')).hexdigest()
 
-            record = DataRecord(
-                project_id=version.project_id,
-                version_id=version.id,
-                stage=version.stage,
-                commit_id=version.commit_id,
-                commit_message=version.commit_message,
-                group_key=group_key,
-                inputs=data["inputs"],
-                outputs=data["outputs"],
-                content_hash=content_hash,
-                author=version.author,
-                # letting server_default handle created_at
-            )
-            self.db.add(record)
+                record = DataRecord(
+                    project_id=version.project_id,
+                    version_id=version.id,
+                    stage=version.stage,
+                    commit_id=version.commit_id,
+                    commit_message=version.commit_message,
+                    group_key=group_key,
+                    inputs=inputs,
+                    outputs=out_list,
+                    content_hash=content_hash,
+                    author=version.author,
+                    # letting server_default handle created_at
+                )
+                self.db.add(record)
             
         self.db.commit()
-        logger.info(f"Aggregated version {version.id} into {len(groups)} DataRecord(s).")
+        logger.info(f"Aggregated version {version.id} into DataRecords (one per output).")
 
     async def process_file_with_sha(
         self,

+ 146 - 91
app/static/records.html

@@ -20,30 +20,34 @@
         }
 
         :root {
-            --bg-base: #f4f7f9;
+            --bg-base: #f8fafc;
             --bg-sidebar: #ffffff;
             --bg-nav: #ffffff;
             --bg-card: #ffffff;
-            --bg-hover: #f8fafc;
-            --bg-active: #eef6ff;
-            --border: #e2e8f0;
-            --border-dark: #d1d5db;
+            --bg-hover: rgba(42, 139, 242, 0.05);
+            --bg-active: rgba(42, 139, 242, 0.1);
+            --border: #edf2f7;
+            --border-dark: #e2e8f0;
             --text-primary: #1e293b;
-            --text-secondary: #475569;
+            --text-secondary: #64748b;
             --text-muted: #94a3b8;
             --accent: #2a8bf2;
-            --radius: 4px;
-            --sidebar-w: 240px;
-            --nav-h: 60px;
+            --radius-sm: 6px;
+            --radius-md: 12px;
+            --radius-lg: 16px;
+            --sidebar-w: 260px;
+            --nav-h: 64px;
+            --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
+            --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
         }
 
         body {
-            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
+            font-family: 'Outfit', 'Inter', -apple-system, sans-serif;
             background: var(--bg-base);
             color: var(--text-primary);
             height: 100vh;
             overflow: hidden;
-            line-height: 1.5;
+            line-height: 1.6;
         }
 
         /* --- Global Layout --- */
@@ -57,21 +61,33 @@
         .navbar {
             height: var(--nav-h);
             background: var(--bg-nav);
-            border-bottom: 2px solid #edeff2;
+            border-bottom: 1px solid var(--border);
             display: flex;
             align-items: center;
-            padding: 0 40px;
+            padding: 0 32px;
             z-index: 100;
             flex-shrink: 0;
+            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
         }
 
         .nav-logo {
             font-size: 20px;
-            font-weight: 700;
-            color: #2c3e50;
-            margin-right: 60px;
+            font-weight: 800;
+            color: #0f172a;
+            margin-right: 48px;
             white-space: nowrap;
-            letter-spacing: 0.5px;
+            letter-spacing: -0.5px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+        }
+
+        .nav-logo::before {
+            content: '';
+            width: 32px;
+            height: 32px;
+            background: linear-gradient(135deg, var(--accent), #6366f1);
+            border-radius: 8px;
         }
 
         .nav-menu {
@@ -87,18 +103,19 @@
             height: 100%;
             display: flex;
             align-items: center;
-            padding: 0 4px;
+            padding: 0 16px;
             position: relative;
         }
 
         .nav-item::after {
             content: '';
             position: absolute;
-            bottom: -2px;
-            left: -4px;
-            right: -4px;
+            bottom: 0;
+            left: 0;
+            right: 0;
             height: 3px;
             background: var(--accent);
+            border-radius: 3px 3px 0 0;
         }
 
         /* --- Main Content Layout --- */
@@ -114,65 +131,63 @@
             background: var(--bg-sidebar);
             border-right: 1px solid var(--border);
             overflow-y: auto;
-            padding: 12px;
-        }
-
-        .sidebar-group {
-            margin-bottom: 8px;
-            border: 1px solid #edeff2;
-            border-radius: 4px;
-            overflow: hidden;
+            padding: 24px 12px;
         }
 
-        .sidebar-tag {
-            font-size: 20px;
+        .sidebar-title {
+            font-size: 11px;
             font-weight: 700;
-            padding: 8px 16px;
-            text-transform: lowercase;
-            width: 100%;
-            border-bottom: 1px solid #edeff2;
-        }
-
-        .tag-how {
-            background: #dcfce7;
-            color: #166534;
-        }
-
-        .tag-what {
-            background: #fef3c7;
-            color: #92400e;
+            color: var(--text-muted);
+            padding: 8px 12px;
+            text-transform: uppercase;
+            letter-spacing: 1px;
+            border-bottom: 1px solid var(--border);
+            margin-bottom: 4px;
         }
 
         .sidebar-list {
             list-style: none;
+            display: flex;
+            flex-direction: column;
+            gap: 4px;
         }
 
         .sidebar-item {
-            padding: 10px 16px;
+            padding: 12px 16px;
             font-size: 14px;
-            color: #4b5563;
+            color: var(--text-secondary);
             cursor: pointer;
             display: flex;
-            justify-content: center;
             align-items: center;
-            transition: all 0.2s;
-            border-bottom: 1px solid #f1f5f9;
+            gap: 12px;
+            transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+            border-radius: var(--radius-md);
+            border: none;
+            font-weight: 500;
         }
 
-        .sidebar-item:last-child {
-            border-bottom: none;
+        .sidebar-item .s-icon {
+            width: 20px;
+            height: 20px;
+            opacity: 0.7;
         }
 
         .sidebar-item:hover {
             background: var(--bg-hover);
             color: var(--text-primary);
+            transform: translateX(4px);
         }
 
         .sidebar-item.active {
-            background: #ffffff;
+            background: var(--bg-active);
             color: var(--accent);
             font-weight: 600;
-            box-shadow: inset 4px 0 0 var(--accent);
+            box-shadow: none;
+        }
+
+        .sidebar-item.active .s-icon {
+            opacity: 1;
+            color: var(--accent);
         }
 
         /* --- Content Area --- */
@@ -180,38 +195,51 @@
             display: flex;
             flex-direction: column;
             overflow: hidden;
+            padding: 24px 32px;
         }
 
         .table-container {
             flex: 1;
             overflow: auto;
-            padding: 24px;
+            background: #ffffff;
+            border-radius: var(--radius-lg);
+            box-shadow: var(--shadow);
+            border: 1px solid var(--border);
         }
 
         .records-table {
             width: 100%;
-            border-collapse: collapse;
-            background: #ffffff;
-            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+            border-collapse: separate;
+            border-spacing: 0;
             min-width: 1000px;
         }
 
         .records-table thead th {
-            background: #eff2f5;
-            color: #1e293b;
-            font-size: 14px;
-            font-weight: 600;
+            background: #f8fafc;
+            color: var(--text-secondary);
+            font-size: 12px;
+            font-weight: 700;
+            text-transform: uppercase;
+            letter-spacing: 0.5px;
             text-align: left;
-            padding: 12px 16px;
-            border: 1px solid var(--border-dark);
+            padding: 16px 20px;
+            border-bottom: 1px solid var(--border);
+            position: sticky;
+            top: 0;
+            z-index: 10;
         }
 
         .records-table td {
-            padding: 12px 16px;
-            border: 1px solid var(--border-dark);
-            vertical-align: middle;
+            padding: 20px;
+            border-bottom: 1px solid var(--border);
+            vertical-align: top;
             font-size: 14px;
-            color: #334155;
+            color: var(--text-primary);
+            transition: background 0.15s;
+        }
+
+        .records-table tr:last-child td {
+            border-bottom: none;
         }
 
         .records-table tr:hover td {
@@ -219,24 +247,34 @@
         }
 
         .id-col {
-            width: 120px;
-            font-family: monospace;
-            color: var(--text-muted);
+            width: 100px;
+        }
+
+        .commit-badge {
+            font-family: 'JetBrains Mono', monospace;
+            background: #f1f5f9;
+            color: var(--text-secondary);
+            padding: 4px 8px;
+            border-radius: var(--radius-sm);
+            font-size: 12px;
+            display: inline-block;
         }
 
         .msg-col {
-            width: 300px;
-            font-weight: 500;
+            width: 260px;
+            font-weight: 600;
+            color: #0f172a;
         }
 
         /* Bubble Tree Hierarchical Styles */
         .bubble-tree {
             background: #ffffff;
-            border: 1px solid var(--border-dark);
-            border-radius: 4px;
-            padding: 4px;
+            border: 1px solid var(--border);
+            border-radius: var(--radius-md);
+            padding: 8px;
             min-height: 48px;
-            min-width: 200px;
+            min-width: 220px;
+            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
         }
 
         .fg-header {
@@ -470,14 +508,7 @@
         <div class="main-container">
             <aside class="sidebar">
                 <div id="sidebarContent">
-                    <div class="sidebar-group">
-                        <div class="sidebar-tag tag-how">how</div>
-                        <ul class="sidebar-list" id="howList"></ul>
-                    </div>
-                    <div class="sidebar-group">
-                        <div class="sidebar-tag tag-what">what</div>
-                        <ul class="sidebar-list" id="whatList"></ul>
-                    </div>
+                    <ul class="sidebar-list" id="stageList"></ul>
                 </div>
             </aside>
 
@@ -498,6 +529,11 @@
             folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
             download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',
             chevron: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>',
+            // Stage icons
+            topic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"></path><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path></svg>',
+            what: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>',
+            craft: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.5 1.5"></path><path d="M14 11l5 5"></path></svg>',
+            gear: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>',
         };
 
         const PAGE_SIZE = 20;
@@ -540,14 +576,33 @@
         }
 
         function renderSidebar() {
-            const howL = $('howList'), whatL = $('whatList');
-            howL.innerHTML = ''; whatL.innerHTML = '';
-            S.stages.forEach(st => {
+            const list = $('stageList');
+            list.innerHTML = '';
+
+            const STAGE_ORDER = ['how选题', 'what选题', 'what创作', 'what制作'];
+            const STAGE_ICONS = {
+                'how选题': IC.topic,
+                'what选题': IC.what,
+                'what创作': IC.craft,
+                'what制作': IC.gear
+            };
+
+            const sortedStages = [...S.stages].sort((a, b) => {
+                const idxA = STAGE_ORDER.indexOf(a.name);
+                const idxB = STAGE_ORDER.indexOf(b.name);
+                if (idxA !== -1 && idxB !== -1) return idxA - idxB;
+                if (idxA !== -1) return -1;
+                if (idxB !== -1) return 1;
+                return a.name.localeCompare(b.name);
+            });
+
+            sortedStages.forEach(st => {
                 const li = document.createElement('li');
                 li.className = 'sidebar-item';
-                li.innerHTML = `${esc(st.name.split('/').pop())}`;
+                const icon = STAGE_ICONS[st.name] || IC.file;
+                li.innerHTML = `<span class="s-icon">${icon}</span>${esc(st.name)}`;
                 li.onclick = () => selectStage(li, st.name);
-                (st.name.toLowerCase().includes('how') || st.name.toLowerCase().includes('test') ? howL : whatL).appendChild(li);
+                list.appendChild(li);
             });
         }
 
@@ -604,7 +659,7 @@
 
             S.records.forEach(r => {
                 h += `<tr>
-                    <td class="id-col">${esc(r.commit_id.substring(0, 8))}</td>
+                    <td class="id-col"><span class="commit-badge">${esc(r.commit_id.substring(0, 8))}</span></td>
                     <td class="msg-col">${esc(r.commit_message || '无描述')}</td>`;
 
                 sortedIn.forEach(l => {

+ 29 - 0
reaggregate_records.py

@@ -0,0 +1,29 @@
+from app.database import SessionLocal
+from app.models import DataVersion
+from app.services.storage_service import StorageService
+from app.services.gogs_client import GogsClient
+import logging
+
+# Setup basic logging to see what's happening
+logging.basicConfig(level=logging.INFO)
+
+def reaggregate():
+    db = SessionLocal()
+    gogs = GogsClient()
+    storage = StorageService(db, gogs)
+    
+    versions = db.query(DataVersion).all()
+    print(f"Found {len(versions)} versions to re-aggregate.")
+    
+    for v in versions:
+        print(f"Re-aggregating version {v.id} (Stage: {v.stage}, Commit: {v.commit_id[:8]})")
+        try:
+            storage.aggregate_version_records(v)
+        except Exception as e:
+            print(f"Error re-aggregating version {v.id}: {e}")
+    
+    db.close()
+    print("Done!")
+
+if __name__ == "__main__":
+    reaggregate()