Skip to content

Instantly share code, notes, and snippets.

@nilsbaier-cmd
Created December 24, 2025 00:59
Show Gist options
  • Select an option

  • Save nilsbaier-cmd/0af690a38528f708b0c6a6f8b58ea4c5 to your computer and use it in GitHub Desktop.

Select an option

Save nilsbaier-cmd/0af690a38528f708b0c6a6f8b58ea4c5 to your computer and use it in GitHub Desktop.
CASA Dashboard v2 - GitHub Actions Setup Files

Data Files

Upload your INAD and BAZL Excel files here via GitHub.com:

  1. Go to this folder on GitHub.com
  2. Click "Add file" > "Upload files"
  3. Drag and drop your Excel files:
    • INAD-Tabelle.xlsx (or .xlsm)
    • BAZL-Daten.xlsx
  4. Click "Commit changes"

The analysis will run automatically within a few minutes, and the dashboard will update with the new data.

File Requirements

INAD-Tabelle

Must contain columns:

  • Fluggesellschaft - Airline code
  • Abflugort - Last stop / Origin airport code
  • Jahr - Year
  • Monat - Month (1-12)
  • Verweigerungsgründe - Refusal code (optional, for filtering)

BAZL-Daten

Must contain columns:

  • Fluggesellschaft - Airline code
  • Abflugort - Airport code
  • PAX - Passenger count
  • Jahr - Year
  • Monat - Month (1-12)
name: Analyze Data and Deploy
on:
# Run when files in data/ folder change
push:
paths:
- 'data/**'
branches:
- main
# Allow manual trigger from GitHub Actions tab
workflow_dispatch:
jobs:
analyze-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pandas openpyxl numpy
- name: Check for data files
id: check_files
run: |
if ls data/*.xlsx 2>/dev/null || ls data/*.xlsm 2>/dev/null; then
echo "has_data=true" >> $GITHUB_OUTPUT
else
echo "has_data=false" >> $GITHUB_OUTPUT
fi
- name: Run analysis
if: steps.check_files.outputs.has_data == 'true'
run: python scripts/generate_analysis.py
- name: Commit analysis results
if: steps.check_files.outputs.has_data == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add public/analysis/
git diff --staged --quiet || git commit -m "Auto-generate analysis from data files"
git push
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build
/**
* CASA Dashboard API Service
* Handles data fetching - supports both static JSON (GitHub Pages) and FastAPI backend
*/
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
// Detect if running on GitHub Pages (static mode) or with backend
const isStaticMode = () => {
// Check if we're on GitHub Pages
if (window.location.hostname.includes('github.io')) return true;
// Check if API_URL is not set (production build without backend)
if (!process.env.REACT_APP_API_URL && process.env.NODE_ENV === 'production') return true;
return false;
};
// Base path for static JSON files
const getStaticBasePath = () => {
const base = process.env.PUBLIC_URL || '';
return `${base}/analysis`;
};
/**
* Fetch static JSON file
*/
const fetchStatic = async (filename) => {
const basePath = getStaticBasePath();
const response = await fetch(`${basePath}/${filename}`);
if (!response.ok) {
throw new Error(`Failed to load ${filename}`);
}
return response.json();
};
/**
* Check API health status
*/
export const checkHealth = async () => {
if (isStaticMode()) {
// In static mode, check if index.json exists
try {
await fetchStatic('index.json');
return { status: 'ok', mode: 'static' };
} catch {
return { status: 'no_data', mode: 'static' };
}
}
const response = await fetch(`${API_URL}/`);
return response.json();
};
/**
* Get current data loading status
*/
export const getStatus = async () => {
if (isStaticMode()) {
try {
const index = await fetchStatic('index.json');
return {
ready: true,
mode: 'static',
semesters: index.semesters,
generated_at: index.generated_at
};
} catch {
return { ready: false, mode: 'static' };
}
}
const response = await fetch(`${API_URL}/api/status`);
return response.json();
};
/**
* Upload INAD and BAZL data files
* Note: Not available in static mode - users should upload via GitHub
*/
export const uploadFiles = async (inadFile, bazlFile) => {
if (isStaticMode()) {
throw new Error('File upload is not available in static mode. Please upload files directly to the GitHub repository.');
}
const formData = new FormData();
formData.append('inad_file', inadFile);
formData.append('bazl_file', bazlFile);
const response = await fetch(`${API_URL}/api/upload`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Upload failed');
}
return response.json();
};
/**
* Load data files from server paths
* Note: Not available in static mode
*/
export const loadServerFiles = async (inadPath, bazlPath) => {
if (isStaticMode()) {
throw new Error('Server file loading is not available in static mode.');
}
const params = new URLSearchParams({
inad_path: inadPath,
bazl_path: bazlPath,
});
const response = await fetch(`${API_URL}/api/load-server-files?${params}`, {
method: 'POST',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to load server files');
}
return response.json();
};
/**
* Get available semesters from loaded data
*/
export const getSemesters = async () => {
if (isStaticMode()) {
return fetchStatic('semesters.json');
}
const response = await fetch(`${API_URL}/api/semesters`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to get semesters');
}
return response.json();
};
/**
* Run analysis for a specific semester
* @param {string} semester - Semester identifier (e.g., "2024-H2")
*/
export const analyzeSemester = async (semester) => {
if (isStaticMode()) {
return fetchStatic(`analysis_${semester}.json`);
}
const response = await fetch(`${API_URL}/api/analyze/${semester}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Analysis failed');
}
return response.json();
};
/**
* Get historic data across multiple semesters
* @param {string[]} semesters - Array of semester identifiers
*/
export const getHistoricData = async (semesters) => {
if (isStaticMode()) {
return fetchStatic('historic.json');
}
const params = new URLSearchParams({
semesters: semesters.join(','),
});
const response = await fetch(`${API_URL}/api/historic?${params}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to get historic data');
}
return response.json();
};
/**
* Detect systemic cases across semesters
* @param {string[]} semesters - Array of semester identifiers
*/
export const getSystemicCases = async (semesters) => {
if (isStaticMode()) {
return fetchStatic('systemic.json');
}
const params = new URLSearchParams({
semesters: semesters.join(','),
});
const response = await fetch(`${API_URL}/api/systemic?${params}`);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to detect systemic cases');
}
return response.json();
};
/**
* Get current analysis configuration
*/
export const getConfig = async () => {
if (isStaticMode()) {
const index = await fetchStatic('index.json');
return index.config || {};
}
const response = await fetch(`${API_URL}/api/config`);
return response.json();
};
/**
* Update analysis configuration
* Note: Not available in static mode
*/
export const updateConfig = async (config) => {
if (isStaticMode()) {
throw new Error('Configuration updates are not available in static mode.');
}
const response = await fetch(`${API_URL}/api/config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to update config');
}
return response.json();
};
/**
* Check if running in static mode (GitHub Pages)
*/
export const isStatic = isStaticMode;
// Export default API object
const api = {
checkHealth,
getStatus,
uploadFiles,
loadServerFiles,
getSemesters,
analyzeSemester,
getHistoricData,
getSystemicCases,
getConfig,
updateConfig,
isStatic,
};
export default api;
import React, { useState, useEffect } from 'react';
import { Settings, Save, RefreshCw, Upload, Server, FileSpreadsheet, Github, ExternalLink } from 'lucide-react';
import { useData } from '../context/DataContext';
const Configuration = ({ translations = {} }) => {
const {
config,
updateConfig,
uploadFiles,
loadServerFiles,
isLoading,
dataReady,
isStaticMode,
runAnalysis,
currentSemester
} = useData();
// Local state for form
const [localConfig, setLocalConfig] = useState({
min_inad: 6,
min_pax: 5000,
min_density: 0.10,
high_priority_multiplier: 1.5,
threshold_method: 'median'
});
// Data source mode
const [dataSource, setDataSource] = useState('upload'); // 'upload' or 'server'
const [inadFile, setInadFile] = useState(null);
const [bazlFile, setBazlFile] = useState(null);
const [inadPath, setInadPath] = useState('');
const [bazlPath, setBazlPath] = useState('');
const [message, setMessage] = useState(null);
// Sync with context config
useEffect(() => {
if (config) {
setLocalConfig(config);
}
}, [config]);
const t = {
pageTitle: translations.configurationTitle || 'Configuration',
pageSubtitle: translations.configurationSubtitle || 'Adjust analysis parameters and data sources',
dataSource: translations.dataSource || 'Data Source',
uploadFiles: translations.uploadFiles || 'Upload Files',
useServerFiles: translations.useServerFiles || 'Use Server Files',
inadFile: translations.inadFile || 'INAD-Tabelle File',
bazlFile: translations.bazlFile || 'BAZL-Daten File',
inadPath: translations.inadPath || 'INAD-Tabelle Path',
bazlPath: translations.bazlPath || 'BAZL-Daten Path',
loadData: translations.loadData || 'Load Data',
analysisParameters: translations.analysisParameters || 'Analysis Parameters',
minInad: translations.minInad || 'Minimum INAD Cases',
minInadDesc: translations.minInadDesc || 'Minimum number of INAD cases for a route to be considered',
minPax: translations.minPax || 'Minimum Passengers',
minPaxDesc: translations.minPaxDesc || 'Minimum passengers for reliable density calculation',
minDensity: translations.minDensity || 'Minimum Density',
minDensityDesc: translations.minDensityDesc || 'Absolute minimum density threshold (per mille)',
multiplier: translations.multiplier || 'High Priority Multiplier',
multiplierDesc: translations.multiplierDesc || 'Multiplier applied to threshold for high priority classification',
thresholdMethod: translations.thresholdMethod || 'Threshold Method',
thresholdMethodDesc: translations.thresholdMethodDesc || 'Statistical method for calculating density threshold',
median: translations.median || 'Median',
trimmedMean: translations.trimmedMean || 'Trimmed Mean',
mean: translations.mean || 'Mean',
saveConfig: translations.saveConfig || 'Save Configuration',
resetDefaults: translations.resetDefaults || 'Reset to Defaults',
dataStatus: translations.dataStatus || 'Data Status',
dataLoaded: translations.dataLoaded || 'Data loaded and ready',
noDataLoaded: translations.noDataLoaded || 'No data loaded',
};
const handleFileUpload = async () => {
if (!inadFile || !bazlFile) {
setMessage({ type: 'error', text: 'Please select both files' });
return;
}
try {
await uploadFiles(inadFile, bazlFile);
setMessage({ type: 'success', text: 'Files uploaded successfully' });
// Run analysis for current semester
if (currentSemester) {
await runAnalysis(currentSemester);
}
} catch (err) {
setMessage({ type: 'error', text: err.message });
}
};
const handleServerLoad = async () => {
if (!inadPath || !bazlPath) {
setMessage({ type: 'error', text: 'Please enter both file paths' });
return;
}
try {
await loadServerFiles(inadPath, bazlPath);
setMessage({ type: 'success', text: 'Server files loaded successfully' });
// Run analysis for current semester
if (currentSemester) {
await runAnalysis(currentSemester);
}
} catch (err) {
setMessage({ type: 'error', text: err.message });
}
};
const handleSaveConfig = async () => {
try {
await updateConfig(localConfig);
setMessage({ type: 'success', text: 'Configuration saved' });
} catch (err) {
setMessage({ type: 'error', text: err.message });
}
};
const handleResetDefaults = () => {
setLocalConfig({
min_inad: 6,
min_pax: 5000,
min_density: 0.10,
high_priority_multiplier: 1.5,
threshold_method: 'median'
});
};
return (
<div>
{/* Page Header */}
<div className="page-header">
<h1 className="page-title">{t.pageTitle}</h1>
<p className="page-subtitle">{t.pageSubtitle}</p>
</div>
{/* Message Banner */}
{message && (
<div style={{
background: message.type === 'error' ? 'var(--color-danger-light)' : 'var(--color-success-light)',
color: message.type === 'error' ? 'var(--color-danger)' : 'var(--color-success)',
padding: '12px 16px',
borderRadius: '8px',
marginBottom: '24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span>{message.text}</span>
<button
onClick={() => setMessage(null)}
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.2rem' }}
>
&times;
</button>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
{/* Data Source Section */}
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
<FileSpreadsheet size={24} style={{ color: 'var(--color-primary)' }} />
<h3 style={{ margin: 0 }}>{t.dataSource}</h3>
</div>
{/* Static Mode - GitHub Upload Instructions */}
{isStaticMode ? (
<div>
<div style={{
background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
padding: '20px',
marginBottom: '20px'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
<Github size={24} style={{ color: 'var(--color-primary)' }} />
<h4 style={{ margin: 0 }}>Upload Data via GitHub</h4>
</div>
<p style={{ color: 'var(--text-secondary)', marginBottom: '16px', fontSize: '0.875rem' }}>
This dashboard runs in static mode. To update the analysis data, upload your Excel files directly to GitHub:
</p>
<ol style={{ color: 'var(--text-secondary)', fontSize: '0.875rem', marginLeft: '20px', marginBottom: '16px' }}>
<li style={{ marginBottom: '8px' }}>Go to the <code>data/</code> folder in the GitHub repository</li>
<li style={{ marginBottom: '8px' }}>Click "Add file" → "Upload files"</li>
<li style={{ marginBottom: '8px' }}>Drag and drop your <strong>INAD-Tabelle.xlsx</strong> and <strong>BAZL-Daten.xlsx</strong> files</li>
<li style={{ marginBottom: '8px' }}>Click "Commit changes"</li>
<li>Wait 2-3 minutes for the analysis to run automatically</li>
</ol>
<a
href="https://github.com/nilsbaier-cmd/casa-dashboard-v2/tree/main/data"
target="_blank"
rel="noopener noreferrer"
className="btn btn-primary"
style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}
>
<Github size={16} />
Open GitHub Data Folder
<ExternalLink size={14} />
</a>
</div>
</div>
) : (
<>
{/* Data Source Toggle */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '20px' }}>
<button
className={`btn ${dataSource === 'upload' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setDataSource('upload')}
>
<Upload size={16} />
{t.uploadFiles}
</button>
<button
className={`btn ${dataSource === 'server' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setDataSource('server')}
>
<Server size={16} />
{t.useServerFiles}
</button>
</div>
{dataSource === 'upload' ? (
<div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
{t.inadFile}
</label>
<input
type="file"
accept=".xlsx,.xlsm,.xls"
onChange={(e) => setInadFile(e.target.files[0])}
style={{
width: '100%',
padding: '8px',
border: '1px solid var(--border-color)',
borderRadius: '8px'
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
{t.bazlFile}
</label>
<input
type="file"
accept=".xlsx,.xlsm,.xls"
onChange={(e) => setBazlFile(e.target.files[0])}
style={{
width: '100%',
padding: '8px',
border: '1px solid var(--border-color)',
borderRadius: '8px'
}}
/>
</div>
<button
className="btn btn-primary"
onClick={handleFileUpload}
disabled={isLoading}
>
<Upload size={16} />
{isLoading ? 'Loading...' : t.loadData}
</button>
</div>
) : (
<div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
{t.inadPath}
</label>
<input
type="text"
value={inadPath}
onChange={(e) => setInadPath(e.target.value)}
placeholder="/path/to/INAD-Tabelle.xlsx"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid var(--border-color)',
borderRadius: '8px'
}}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
{t.bazlPath}
</label>
<input
type="text"
value={bazlPath}
onChange={(e) => setBazlPath(e.target.value)}
placeholder="/path/to/BAZL-Daten.xlsx"
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid var(--border-color)',
borderRadius: '8px'
}}
/>
</div>
<button
className="btn btn-primary"
onClick={handleServerLoad}
disabled={isLoading}
>
<Server size={16} />
{isLoading ? 'Loading...' : t.loadData}
</button>
</div>
)}
</>
)}
{/* Data Status */}
<div style={{
marginTop: '20px',
padding: '12px',
background: dataReady ? 'var(--color-success-light)' : 'var(--bg-tertiary)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<div style={{
width: '8px',
height: '8px',
borderRadius: '50%',
background: dataReady ? 'var(--color-success)' : 'var(--text-muted)'
}} />
<span style={{ fontSize: '0.875rem' }}>
{dataReady ? t.dataLoaded : t.noDataLoaded}
</span>
</div>
</div>
{/* Analysis Parameters Section */}
<div className="card">
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
<Settings size={24} style={{ color: 'var(--color-primary)' }} />
<h3 style={{ margin: 0 }}>{t.analysisParameters}</h3>
</div>
{/* Min INAD */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontWeight: 500 }}>{t.minInad}</span>
<span style={{ fontWeight: 600, color: 'var(--color-primary)' }}>{localConfig.min_inad}</span>
</label>
<input
type="range"
min="1"
max="20"
value={localConfig.min_inad}
onChange={(e) => setLocalConfig({ ...localConfig, min_inad: parseInt(e.target.value) })}
style={{ width: '100%' }}
/>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '4px' }}>
{t.minInadDesc}
</p>
</div>
{/* Min PAX */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontWeight: 500 }}>{t.minPax}</span>
<span style={{ fontWeight: 600, color: 'var(--color-primary)' }}>{localConfig.min_pax.toLocaleString()}</span>
</label>
<input
type="range"
min="1000"
max="20000"
step="1000"
value={localConfig.min_pax}
onChange={(e) => setLocalConfig({ ...localConfig, min_pax: parseInt(e.target.value) })}
style={{ width: '100%' }}
/>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '4px' }}>
{t.minPaxDesc}
</p>
</div>
{/* Threshold Method */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}>
{t.thresholdMethod}
</label>
<select
value={localConfig.threshold_method}
onChange={(e) => setLocalConfig({ ...localConfig, threshold_method: e.target.value })}
className="semester-select"
>
<option value="median">{t.median}</option>
<option value="trimmed_mean">{t.trimmedMean}</option>
<option value="mean">{t.mean}</option>
</select>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '4px' }}>
{t.thresholdMethodDesc}
</p>
</div>
{/* High Priority Multiplier */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontWeight: 500 }}>{t.multiplier}</span>
<span style={{ fontWeight: 600, color: 'var(--color-primary)' }}>{localConfig.high_priority_multiplier.toFixed(1)}x</span>
</label>
<input
type="range"
min="1.0"
max="3.0"
step="0.1"
value={localConfig.high_priority_multiplier}
onChange={(e) => setLocalConfig({ ...localConfig, high_priority_multiplier: parseFloat(e.target.value) })}
style={{ width: '100%' }}
/>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginTop: '4px' }}>
{t.multiplierDesc}
</p>
</div>
{/* Action Buttons */}
<div style={{ display: 'flex', gap: '12px' }}>
<button className="btn btn-primary" onClick={handleSaveConfig} disabled={isLoading}>
<Save size={16} />
{t.saveConfig}
</button>
<button className="btn btn-secondary" onClick={handleResetDefaults}>
<RefreshCw size={16} />
{t.resetDefaults}
</button>
</div>
</div>
</div>
</div>
);
};
export default Configuration;
/**
* CASA Dashboard Data Context
* Global state management for analysis data
* Supports both static JSON (GitHub Pages) and FastAPI backend
*/
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import api from '../services/api';
// Create context
const DataContext = createContext(null);
// Provider component
export const DataProvider = ({ children }) => {
// Data loading state
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [dataReady, setDataReady] = useState(false);
// Static mode detection
const [isStaticMode, setIsStaticMode] = useState(false);
// Available semesters
const [semesters, setSemesters] = useState([]);
// Current semester selection
const [currentSemester, setCurrentSemester] = useState(null);
// Analysis results
const [analysisData, setAnalysisData] = useState(null);
const [historicData, setHistoricData] = useState(null);
const [systemicCases, setSystemicCases] = useState(null);
// Configuration
const [config, setConfig] = useState({
min_inad: 6,
min_pax: 5000,
min_density: 0.10,
high_priority_multiplier: 1.5,
threshold_method: 'median',
});
// Initialize - check for static mode and auto-load data
useEffect(() => {
const initialize = async () => {
const staticMode = api.isStatic();
setIsStaticMode(staticMode);
if (staticMode) {
setIsLoading(true);
try {
// Try to load semesters from static JSON
const semesterData = await api.getSemesters();
if (semesterData && semesterData.length > 0) {
setSemesters(semesterData);
setDataReady(true);
// Auto-select latest semester
const latest = semesterData[semesterData.length - 1];
setCurrentSemester(latest.value);
// Load analysis for latest semester
const analysis = await api.analyzeSemester(latest.value);
setAnalysisData(analysis);
if (analysis.config) {
setConfig(analysis.config);
}
}
} catch (err) {
// No static data available - that's ok, just show empty state
console.log('No static analysis data available yet');
} finally {
setIsLoading(false);
}
}
};
initialize();
}, []);
// Upload files handler
const uploadFiles = useCallback(async (inadFile, bazlFile) => {
setIsLoading(true);
setError(null);
try {
const result = await api.uploadFiles(inadFile, bazlFile);
setSemesters(result.semesters || []);
setDataReady(true);
// Auto-select latest semester
if (result.semesters && result.semesters.length > 0) {
const latest = result.semesters[result.semesters.length - 1];
setCurrentSemester(latest.value);
}
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, []);
// Load server files handler
const loadServerFiles = useCallback(async (inadPath, bazlPath) => {
setIsLoading(true);
setError(null);
try {
const result = await api.loadServerFiles(inadPath, bazlPath);
setSemesters(result.semesters || []);
setDataReady(true);
// Auto-select latest semester
if (result.semesters && result.semesters.length > 0) {
const latest = result.semesters[result.semesters.length - 1];
setCurrentSemester(latest.value);
}
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, []);
// Run analysis for current semester
const runAnalysis = useCallback(async (semester = currentSemester) => {
if (!semester) {
setError('No semester selected');
return null;
}
setIsLoading(true);
setError(null);
try {
const result = await api.analyzeSemester(semester);
setAnalysisData(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, [currentSemester]);
// Get historic data
const fetchHistoricData = useCallback(async (semesterList = null) => {
const sems = semesterList || semesters.map(s => s.value);
if (sems.length === 0) return null;
setIsLoading(true);
setError(null);
try {
const result = await api.getHistoricData(sems);
setHistoricData(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, [semesters]);
// Get systemic cases
const fetchSystemicCases = useCallback(async (semesterList = null) => {
const sems = semesterList || semesters.map(s => s.value);
if (sems.length === 0) return null;
setIsLoading(true);
setError(null);
try {
const result = await api.getSystemicCases(sems);
setSystemicCases(result);
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, [semesters]);
// Update configuration
const updateConfig = useCallback(async (newConfig) => {
if (isStaticMode) {
setError('Configuration updates are not available in static mode.');
return;
}
setIsLoading(true);
setError(null);
try {
const result = await api.updateConfig(newConfig);
setConfig(result.config);
// Re-run analysis with new config
if (currentSemester) {
await runAnalysis(currentSemester);
}
return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setIsLoading(false);
}
}, [currentSemester, runAnalysis, isStaticMode]);
// Change semester
const changeSemester = useCallback(async (semester) => {
setCurrentSemester(semester);
await runAnalysis(semester);
}, [runAnalysis]);
// Clear error
const clearError = useCallback(() => {
setError(null);
}, []);
// Context value
const value = {
// State
isLoading,
error,
dataReady,
isStaticMode,
semesters,
currentSemester,
analysisData,
historicData,
systemicCases,
config,
// Actions
uploadFiles,
loadServerFiles,
runAnalysis,
fetchHistoricData,
fetchSystemicCases,
updateConfig,
changeSemester,
clearError,
setCurrentSemester,
};
return (
<DataContext.Provider value={value}>
{children}
</DataContext.Provider>
);
};
// Custom hook for using the context
export const useData = () => {
const context = useContext(DataContext);
if (!context) {
throw new Error('useData must be used within a DataProvider');
}
return context;
};
export default DataContext;
#!/usr/bin/env python3
"""
Generate static JSON analysis files from INAD and BAZL Excel data.
This script is run by GitHub Actions when new data is uploaded.
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
# Add backend to path
sys.path.insert(0, str(Path(__file__).parent.parent / 'backend'))
from inad_analysis import (
AnalysisConfig,
run_full_analysis,
get_available_semesters,
detect_systemic_cases
)
from geography import enrich_routes_with_coordinates
def find_data_files(data_dir):
"""Find INAD and BAZL files in the data directory."""
inad_file = None
bazl_file = None
for file in os.listdir(data_dir):
lower = file.lower()
if 'inad' in lower and (lower.endswith('.xlsx') or lower.endswith('.xlsm')):
inad_file = os.path.join(data_dir, file)
elif 'bazl' in lower and (lower.endswith('.xlsx') or lower.endswith('.xlsm')):
bazl_file = os.path.join(data_dir, file)
return inad_file, bazl_file
def analyze_semester(inad_path, bazl_path, semester, config):
"""Run analysis for a single semester."""
year, half = semester.split('-')
year = int(year)
if half == 'H1':
start_date = datetime(year, 1, 1)
end_date = datetime(year, 6, 30)
else:
start_date = datetime(year, 7, 1)
end_date = datetime(year, 12, 31)
results = run_full_analysis(inad_path, bazl_path, start_date, end_date, config)
# Enrich with coordinates
step3_df = results['step3']
step3_enriched = enrich_routes_with_coordinates(step3_df)
# Convert to JSON-friendly format
routes = []
for _, row in step3_enriched.iterrows():
routes.append({
'airline': row['Airline'],
'lastStop': row['LastStop'],
'inad': int(row['INAD_Count']),
'pax': int(row['PAX']),
'density': round(row['Density'], 4) if row['Density'] else None,
'confidence': int(row['Confidence']),
'priority': row['Priority'],
'originLat': row.get('OriginLat'),
'originLng': row.get('OriginLng'),
'originCity': row.get('OriginCity', ''),
'originCountry': row.get('OriginCountry', '')
})
# Step 1 airlines
airlines = []
for _, row in results['step1'].iterrows():
airlines.append({
'airline': row['Airline'],
'inadCount': int(row['INAD_Count'])
})
# Step 2 routes
step2_routes = []
for _, row in results['step2'].iterrows():
step2_routes.append({
'airline': row['Airline'],
'lastStop': row['LastStop'],
'inadCount': int(row['INAD_Count'])
})
return {
'semester': semester,
'summary': results['summary'],
'threshold': round(results['threshold'], 4),
'routes': routes,
'airlines': airlines,
'step2Routes': step2_routes,
'config': {
'min_inad': config.min_inad,
'min_pax': config.min_pax,
'min_density': config.min_density,
'threshold_method': config.threshold_method,
'high_priority_multiplier': config.high_priority_multiplier
},
'generated_at': datetime.now().isoformat()
}
def generate_historic_data(semester_results):
"""Generate historic trend data from semester results."""
semesters = []
for semester, result in semester_results.items():
semesters.append({
'semester': semester,
'summary': result['summary'],
'threshold': result['threshold'],
'highPriorityCount': result['summary']['high_priority'],
'watchListCount': result['summary']['watch_list'],
'totalInad': result['summary']['total_inad']
})
# Sort by semester
semesters.sort(key=lambda x: x['semester'])
# Calculate trend
if len(semesters) >= 2:
first = semesters[0]
last = semesters[-1]
hp_change = last['highPriorityCount'] - first['highPriorityCount']
wl_change = last['watchListCount'] - first['watchListCount']
total_change = hp_change + wl_change
if total_change > 0:
direction = 'worsening'
elif total_change < 0:
direction = 'improving'
else:
direction = 'stable'
trend = {
'direction': direction,
'highPriorityChange': hp_change,
'watchListChange': wl_change,
'totalChange': total_change
}
else:
trend = {'direction': 'stable', 'highPriorityChange': 0, 'watchListChange': 0, 'totalChange': 0}
return {
'semesters': semesters,
'trend': trend,
'generated_at': datetime.now().isoformat()
}
def generate_systemic_cases(inad_path, bazl_path, semesters_info, config):
"""Generate systemic cases data."""
import pandas as pd
semester_results = []
for sem_info in semesters_info:
semester = sem_info['value']
year, half = semester.split('-')
year = int(year)
if half == 'H1':
start_date = datetime(year, 1, 1)
end_date = datetime(year, 6, 30)
else:
start_date = datetime(year, 7, 1)
end_date = datetime(year, 12, 31)
results = run_full_analysis(inad_path, bazl_path, start_date, end_date, config)
semester_results.append((semester, results['step3']))
# Detect systemic cases
systemic_df = detect_systemic_cases(semester_results, config)
cases = []
for _, row in systemic_df.iterrows():
cases.append({
'airline': row['Airline'],
'lastStop': row['LastStop'],
'appearances': int(row['Appearances']),
'consecutive': bool(row['Consecutive']),
'trend': row['Trend'],
'latestPriority': row['LatestPriority']
})
return {
'cases': cases,
'generated_at': datetime.now().isoformat()
}
def main():
# Paths
project_root = Path(__file__).parent.parent
data_dir = project_root / 'data'
output_dir = project_root / 'public' / 'analysis'
# Ensure output directory exists
output_dir.mkdir(parents=True, exist_ok=True)
# Find data files
inad_file, bazl_file = find_data_files(data_dir)
if not inad_file or not bazl_file:
print("Error: Could not find INAD and BAZL files in data/ directory")
print(f" INAD file: {inad_file}")
print(f" BAZL file: {bazl_file}")
sys.exit(1)
print(f"Found INAD file: {inad_file}")
print(f"Found BAZL file: {bazl_file}")
# Configuration
config = AnalysisConfig()
# Get available semesters
semesters = get_available_semesters(inad_file)
print(f"Found {len(semesters)} semesters: {[s['value'] for s in semesters]}")
# Save semesters list
with open(output_dir / 'semesters.json', 'w') as f:
json.dump(semesters, f, indent=2)
print("Generated: semesters.json")
# Analyze each semester
semester_results = {}
for sem_info in semesters:
semester = sem_info['value']
print(f"Analyzing {semester}...")
try:
result = analyze_semester(inad_file, bazl_file, semester, config)
semester_results[semester] = result
# Save individual semester analysis
with open(output_dir / f'analysis_{semester}.json', 'w') as f:
json.dump(result, f, indent=2)
print(f" Generated: analysis_{semester}.json")
except Exception as e:
print(f" Error analyzing {semester}: {e}")
# Generate historic data
if semester_results:
historic = generate_historic_data(semester_results)
with open(output_dir / 'historic.json', 'w') as f:
json.dump(historic, f, indent=2)
print("Generated: historic.json")
# Generate systemic cases
if len(semesters) >= 2:
print("Detecting systemic cases...")
try:
systemic = generate_systemic_cases(inad_file, bazl_file, semesters, config)
with open(output_dir / 'systemic.json', 'w') as f:
json.dump(systemic, f, indent=2)
print("Generated: systemic.json")
except Exception as e:
print(f"Error generating systemic cases: {e}")
# Generate index file with metadata
index = {
'semesters': [s['value'] for s in semesters],
'latest_semester': semesters[-1]['value'] if semesters else None,
'generated_at': datetime.now().isoformat(),
'config': {
'min_inad': config.min_inad,
'min_pax': config.min_pax,
'min_density': config.min_density,
'threshold_method': config.threshold_method,
'high_priority_multiplier': config.high_priority_multiplier
}
}
with open(output_dir / 'index.json', 'w') as f:
json.dump(index, f, indent=2)
print("Generated: index.json")
print("\nAnalysis complete!")
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment