| <!DOCTYPE html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="UTF-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| <title>Cancer@Home v2 - Dashboard</title>
|
| <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
|
| <style>
|
| * {
|
| margin: 0;
|
| padding: 0;
|
| box-sizing: border-box;
|
| }
|
|
|
| body {
|
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: #333;
|
| min-height: 100vh;
|
| }
|
|
|
| .header {
|
| background: rgba(0, 0, 0, 0.2);
|
| color: white;
|
| padding: 20px;
|
| text-align: center;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .header h1 {
|
| font-size: 2.5em;
|
| margin-bottom: 10px;
|
| }
|
|
|
| .header p {
|
| opacity: 0.9;
|
| }
|
|
|
| .container {
|
| max-width: 1400px;
|
| margin: 20px auto;
|
| padding: 0 20px;
|
| }
|
|
|
| .tabs {
|
| display: flex;
|
| gap: 10px;
|
| margin-bottom: 20px;
|
| flex-wrap: wrap;
|
| }
|
|
|
| .tab-button {
|
| background: rgba(255, 255, 255, 0.9);
|
| border: none;
|
| padding: 15px 30px;
|
| border-radius: 8px;
|
| cursor: pointer;
|
| font-size: 16px;
|
| font-weight: 500;
|
| transition: all 0.3s;
|
| }
|
|
|
| .tab-button:hover {
|
| background: white;
|
| transform: translateY(-2px);
|
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| }
|
|
|
| .tab-button.active {
|
| background: white;
|
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| }
|
|
|
| .tab-content {
|
| display: none;
|
| }
|
|
|
| .tab-content.active {
|
| display: block;
|
| }
|
|
|
| .cards {
|
| display: grid;
|
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| gap: 20px;
|
| margin-bottom: 30px;
|
| }
|
|
|
| .card {
|
| background: white;
|
| border-radius: 12px;
|
| padding: 25px;
|
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| }
|
|
|
| .card h3 {
|
| color: #667eea;
|
| margin-bottom: 15px;
|
| font-size: 1.3em;
|
| }
|
|
|
| .stat {
|
| font-size: 2.5em;
|
| font-weight: bold;
|
| color: #764ba2;
|
| margin: 10px 0;
|
| }
|
|
|
| .graph-container {
|
| background: white;
|
| border-radius: 12px;
|
| padding: 25px;
|
| margin-bottom: 20px;
|
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| }
|
|
|
| #neo4j-viz {
|
| width: 100%;
|
| height: 600px;
|
| border: 2px solid #e0e0e0;
|
| border-radius: 8px;
|
| }
|
|
|
| .button {
|
| background: #667eea;
|
| color: white;
|
| border: none;
|
| padding: 12px 24px;
|
| border-radius: 6px;
|
| cursor: pointer;
|
| font-size: 16px;
|
| transition: background 0.3s;
|
| }
|
|
|
| .button:hover {
|
| background: #5568d3;
|
| }
|
|
|
| .task-list {
|
| list-style: none;
|
| }
|
|
|
| .task-item {
|
| background: #f5f5f5;
|
| padding: 15px;
|
| margin: 10px 0;
|
| border-radius: 6px;
|
| border-left: 4px solid #667eea;
|
| }
|
|
|
| .task-item.completed {
|
| border-left-color: #4caf50;
|
| }
|
|
|
| .task-item.running {
|
| border-left-color: #ff9800;
|
| }
|
|
|
| .status-badge {
|
| display: inline-block;
|
| padding: 4px 12px;
|
| border-radius: 12px;
|
| font-size: 12px;
|
| font-weight: 600;
|
| text-transform: uppercase;
|
| }
|
|
|
| .status-pending { background: #ffc107; color: #000; }
|
| .status-running { background: #2196f3; color: white; }
|
| .status-completed { background: #4caf50; color: white; }
|
| .status-failed { background: #f44336; color: white; }
|
|
|
| .input-group {
|
| margin: 15px 0;
|
| }
|
|
|
| .input-group label {
|
| display: block;
|
| margin-bottom: 5px;
|
| font-weight: 500;
|
| }
|
|
|
| .input-group input, .input-group select {
|
| width: 100%;
|
| padding: 10px;
|
| border: 1px solid #ddd;
|
| border-radius: 6px;
|
| font-size: 14px;
|
| }
|
|
|
| .project-card {
|
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| color: white;
|
| padding: 20px;
|
| border-radius: 8px;
|
| margin: 10px 0;
|
| cursor: pointer;
|
| transition: transform 0.2s;
|
| }
|
|
|
| .project-card:hover {
|
| transform: translateY(-3px);
|
| }
|
|
|
| .loading {
|
| text-align: center;
|
| padding: 40px;
|
| color: #667eea;
|
| }
|
|
|
| @keyframes spin {
|
| to { transform: rotate(360deg); }
|
| }
|
|
|
| .spinner {
|
| border: 4px solid #f3f3f3;
|
| border-top: 4px solid #667eea;
|
| border-radius: 50%;
|
| width: 40px;
|
| height: 40px;
|
| animation: spin 1s linear infinite;
|
| margin: 20px auto;
|
| }
|
| </style>
|
| </head>
|
| <body>
|
| <div class="header">
|
| <h1>🧬 Cancer@Home v2</h1>
|
| <p>Distributed Cancer Genomics Research Platform</p>
|
| </div>
|
|
|
| <div class="container">
|
| <div class="tabs">
|
| <button class="tab-button active" onclick="showTab('dashboard')">📊 Dashboard</button>
|
| <button class="tab-button" onclick="showTab('neo4j')">🔍 Neo4j Visualization</button>
|
| <button class="tab-button" onclick="showTab('boinc')">⚡ BOINC Tasks</button>
|
| <button class="tab-button" onclick="showTab('gdc')">📚 GDC Data</button>
|
| <button class="tab-button" onclick="showTab('pipeline')">🧪 Analysis Pipeline</button>
|
| </div>
|
|
|
|
|
| <div id="dashboard" class="tab-content active">
|
| <div class="cards" id="stats-cards">
|
| <div class="card">
|
| <h3>Total Genes</h3>
|
| <div class="stat" id="total-genes">-</div>
|
| </div>
|
| <div class="card">
|
| <h3>Total Mutations</h3>
|
| <div class="stat" id="total-mutations">-</div>
|
| </div>
|
| <div class="card">
|
| <h3>Total Patients</h3>
|
| <div class="stat" id="total-patients">-</div>
|
| </div>
|
| <div class="card">
|
| <h3>Cancer Types</h3>
|
| <div class="stat" id="total-cancer-types">-</div>
|
| </div>
|
| </div>
|
|
|
| <div class="graph-container">
|
| <h3>Mutation Distribution by Cancer Type</h3>
|
| <canvas id="mutation-chart"></canvas>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="neo4j" class="tab-content">
|
| <div class="graph-container">
|
| <h3>Cancer Genomics Knowledge Graph</h3>
|
| <div id="neo4j-viz"></div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="boinc" class="tab-content">
|
| <div class="cards">
|
| <div class="card">
|
| <h3>Submit New Task</h3>
|
| <div class="input-group">
|
| <label>Task Type</label>
|
| <select id="task-type">
|
| <option value="variant_calling">Variant Calling</option>
|
| <option value="blast_search">BLAST Search</option>
|
| <option value="alignment">Sequence Alignment</option>
|
| </select>
|
| </div>
|
| <div class="input-group">
|
| <label>Input File</label>
|
| <input type="text" id="input-file" placeholder="path/to/input.fastq">
|
| </div>
|
| <button class="button" onclick="submitBoincTask()">Submit Task</button>
|
| </div>
|
|
|
| <div class="card">
|
| <h3>BOINC Statistics</h3>
|
| <div id="boinc-stats"></div>
|
| </div>
|
| </div>
|
|
|
| <div class="card">
|
| <h3>Active Tasks</h3>
|
| <ul class="task-list" id="task-list"></ul>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="gdc" class="tab-content">
|
| <div class="card">
|
| <h3>Available GDC Projects</h3>
|
| <div id="gdc-projects"></div>
|
| </div>
|
| </div>
|
|
|
|
|
| <div id="pipeline" class="tab-content">
|
| <div class="cards">
|
| <div class="card">
|
| <h3>FASTQ Quality Control</h3>
|
| <p>Run quality control analysis on sequencing data</p>
|
| <button class="button" style="margin-top: 15px;">Run QC</button>
|
| </div>
|
| <div class="card">
|
| <h3>BLAST Search</h3>
|
| <p>Perform sequence alignment and homology search</p>
|
| <button class="button" style="margin-top: 15px;">Run BLAST</button>
|
| </div>
|
| <div class="card">
|
| <h3>Variant Calling</h3>
|
| <p>Identify genetic variants from sequencing data</p>
|
| <button class="button" style="margin-top: 15px;">Call Variants</button>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <script>
|
|
|
| function showTab(tabName) {
|
| document.querySelectorAll('.tab-content').forEach(tab => {
|
| tab.classList.remove('active');
|
| });
|
| document.querySelectorAll('.tab-button').forEach(btn => {
|
| btn.classList.remove('active');
|
| });
|
|
|
| document.getElementById(tabName).classList.add('active');
|
| event.target.classList.add('active');
|
|
|
| if (tabName === 'dashboard') loadDashboard();
|
| else if (tabName === 'neo4j') loadNeo4jViz();
|
| else if (tabName === 'boinc') loadBoincTasks();
|
| else if (tabName === 'gdc') loadGdcProjects();
|
| }
|
|
|
|
|
| async function loadDashboard() {
|
| try {
|
| const response = await fetch('/api/neo4j/summary');
|
| const data = await response.json();
|
|
|
| document.getElementById('total-genes').textContent = data.genes || 0;
|
| document.getElementById('total-mutations').textContent = data.mutations || 0;
|
| document.getElementById('total-patients').textContent = data.patients || 0;
|
| document.getElementById('total-cancer-types').textContent = data.cancer_types || 0;
|
|
|
| createMutationChart();
|
| } catch (error) {
|
| console.error('Error loading dashboard:', error);
|
| }
|
| }
|
|
|
|
|
| function createMutationChart() {
|
| const ctx = document.getElementById('mutation-chart').getContext('2d');
|
| new Chart(ctx, {
|
| type: 'bar',
|
| data: {
|
| labels: ['Breast Cancer', 'Lung Adenocarcinoma', 'Colon Adenocarcinoma', 'Glioblastoma'],
|
| datasets: [{
|
| label: 'Mutations',
|
| data: [245, 189, 156, 203],
|
| backgroundColor: [
|
| 'rgba(102, 126, 234, 0.8)',
|
| 'rgba(118, 75, 162, 0.8)',
|
| 'rgba(237, 100, 166, 0.8)',
|
| 'rgba(255, 154, 158, 0.8)'
|
| ]
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: true,
|
| plugins: {
|
| legend: { display: false }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| function loadNeo4jViz() {
|
| const viz = document.getElementById('neo4j-viz');
|
| viz.innerHTML = '<div class="loading"><div class="spinner"></div><p>Loading graph visualization...</p></div>';
|
|
|
|
|
| setTimeout(() => {
|
| const width = viz.clientWidth;
|
| const height = 600;
|
|
|
| viz.innerHTML = '';
|
| const svg = d3.select('#neo4j-viz')
|
| .append('svg')
|
| .attr('width', width)
|
| .attr('height', height);
|
|
|
|
|
| const nodes = [
|
| { id: 'TP53', type: 'gene', x: width/2, y: height/2 },
|
| { id: 'BRCA1', type: 'gene', x: width/3, y: height/3 },
|
| { id: 'KRAS', type: 'gene', x: 2*width/3, y: height/3 },
|
| { id: 'Patient 1', type: 'patient', x: width/4, y: 3*height/4 },
|
| { id: 'Patient 2', type: 'patient', x: 3*width/4, y: 3*height/4 },
|
| { id: 'Breast Cancer', type: 'cancer', x: width/2, y: height/4 }
|
| ];
|
|
|
| const links = [
|
| { source: 'Patient 1', target: 'TP53' },
|
| { source: 'Patient 1', target: 'Breast Cancer' },
|
| { source: 'Patient 2', target: 'KRAS' },
|
| { source: 'TP53', target: 'Breast Cancer' }
|
| ];
|
|
|
|
|
| svg.selectAll('line')
|
| .data(links)
|
| .enter()
|
| .append('line')
|
| .attr('x1', d => nodes.find(n => n.id === d.source).x)
|
| .attr('y1', d => nodes.find(n => n.id === d.source).y)
|
| .attr('x2', d => nodes.find(n => n.id === d.target).x)
|
| .attr('y2', d => nodes.find(n => n.id === d.target).y)
|
| .attr('stroke', '#999')
|
| .attr('stroke-width', 2);
|
|
|
|
|
| svg.selectAll('circle')
|
| .data(nodes)
|
| .enter()
|
| .append('circle')
|
| .attr('cx', d => d.x)
|
| .attr('cy', d => d.y)
|
| .attr('r', 20)
|
| .attr('fill', d => {
|
| if (d.type === 'gene') return '#667eea';
|
| if (d.type === 'patient') return '#764ba2';
|
| return '#ed64a6';
|
| });
|
|
|
|
|
| svg.selectAll('text')
|
| .data(nodes)
|
| .enter()
|
| .append('text')
|
| .attr('x', d => d.x)
|
| .attr('y', d => d.y - 25)
|
| .attr('text-anchor', 'middle')
|
| .text(d => d.id)
|
| .attr('font-size', '12px')
|
| .attr('fill', '#333');
|
| }, 500);
|
| }
|
|
|
|
|
| async function loadBoincTasks() {
|
| try {
|
| const [tasksResponse, statsResponse] = await Promise.all([
|
| fetch('/api/boinc/tasks'),
|
| fetch('/api/boinc/statistics')
|
| ]);
|
|
|
| const tasksData = await tasksResponse.json();
|
| const statsData = await statsResponse.json();
|
|
|
|
|
| const taskList = document.getElementById('task-list');
|
| taskList.innerHTML = tasksData.tasks.map(task => `
|
| <li class="task-item ${task.status}">
|
| <strong>${task.name}</strong>
|
| <span class="status-badge status-${task.status}">${task.status}</span>
|
| <div style="margin-top: 8px; font-size: 14px; color: #666;">
|
| Type: ${task.workunit_type} | Created: ${new Date(task.created_at).toLocaleString()}
|
| </div>
|
| </li>
|
| `).join('');
|
|
|
|
|
| const statsDiv = document.getElementById('boinc-stats');
|
| statsDiv.innerHTML = `
|
| <p><strong>Total Tasks:</strong> ${statsData.total_tasks}</p>
|
| <p><strong>Completed:</strong> ${statsData.by_status?.completed || 0}</p>
|
| <p><strong>Running:</strong> ${statsData.by_status?.running || 0}</p>
|
| <p><strong>Pending:</strong> ${statsData.by_status?.pending || 0}</p>
|
| `;
|
| } catch (error) {
|
| console.error('Error loading BOINC tasks:', error);
|
| }
|
| }
|
|
|
|
|
| async function submitBoincTask() {
|
| const taskType = document.getElementById('task-type').value;
|
| const inputFile = document.getElementById('input-file').value;
|
|
|
| if (!inputFile) {
|
| alert('Please provide an input file path');
|
| return;
|
| }
|
|
|
| try {
|
| const response = await fetch('/api/boinc/submit', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ workunit_type: taskType, input_file: inputFile })
|
| });
|
|
|
| const data = await response.json();
|
| alert(`Task submitted successfully! Task ID: ${data.task_id}`);
|
| loadBoincTasks();
|
| } catch (error) {
|
| console.error('Error submitting task:', error);
|
| alert('Failed to submit task');
|
| }
|
| }
|
|
|
|
|
| async function loadGdcProjects() {
|
| try {
|
| const response = await fetch('/api/gdc/projects');
|
| const data = await response.json();
|
|
|
| const projectsDiv = document.getElementById('gdc-projects');
|
| projectsDiv.innerHTML = data.projects.map(project => `
|
| <div class="project-card">
|
| <h4>${project.name}</h4>
|
| <p>Project ID: ${project.id}</p>
|
| <p>Cases: ${project.cases}</p>
|
| </div>
|
| `).join('');
|
| } catch (error) {
|
| console.error('Error loading GDC projects:', error);
|
| }
|
| }
|
|
|
|
|
| window.onload = () => {
|
| loadDashboard();
|
| };
|
| </script>
|
| </body>
|
| </html>
|
|
|