| | <!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>
|
| |
|