This document serves as the primary development guide for the Solar Bitcoin Mining Calculator project. It establishes coding standards, architectural patterns, and development workflows to ensure maintainable, scalable, and high-quality code.
- Project Architecture
- Current Implementation Status
- TypeScript Standards
- React Development Patterns
- Cloudflare Workers Best Practices
- Database Design with D1
- Build System with Vite
- Code Organization
- Testing Standards
- Error Handling
- Performance Guidelines
- Development Workflow
- Future Roadmap
src/
├── client/ # React frontend application
│ ├── components/ # React components
│ │ ├── ui/ # Basic UI components
│ │ ├── forms/ # Form components
│ │ ├── charts/ # Data visualization
│ │ └── layout/ # Layout components
│ ├── hooks/ # Custom React hooks
│ ├── pages/ # Page-level components
│ ├── services/ # API client services
│ ├── types/ # Client-specific types
│ └── utils/ # Client-side utilities
├── server/ # Cloudflare Workers backend (3-worker architecture)
│ ├── api/ # Main API Worker (port 8787)
│ │ ├── handlers/ # Route handlers
│ │ ├── services/ # Business logic
│ │ ├── models/ # Data models
│ │ └── utils/ # API utilities
│ ├── calculations/ # Calculation Worker (port 8788)
│ │ ├── engines/ # Calculation engines
│ │ ├── services/ # Calculation services
│ │ └── utils/ # Calculation utilities
│ ├── data/ # Data Worker (port 8789)
│ │ ├── providers/ # External API integrations
│ │ ├── cache/ # Data caching
│ │ ├── schedulers/ # Scheduled jobs
│ │ └── utils/ # Data utilities
│ ├── shared/ # Shared server code
│ │ ├── database/ # Database layer
│ │ │ ├── migrations/ # SQL migrations
│ │ │ └── repository-base.ts
│ │ ├── errors/ # Error classes
│ │ ├── middleware/ # Shared middleware
│ │ ├── utils/ # Shared utilities
│ │ └── validation/ # Data validation
│ └── types/ # Server-wide types
└── shared/ # Code shared between client and server
├── types/ # Common TypeScript interfaces
├── constants/ # Shared constants
├── config/ # Configuration utilities
└── monitoring/ # Telemetry and monitoring
- API Worker (
wrangler.api.toml): Handles UI interactions, CRUD operations, routing - Calculation Worker (
wrangler.calculations.toml): Performs heavy computational tasks and projections - Data Worker (
wrangler.data.toml): Manages external API calls, caching, and scheduled jobs - Independent Scaling: Each worker scales based on its specific workload
- Isolated Failures: Issues in one worker don't affect others
- Optimized Performance: Different CPU limits and resources per worker type
- Development: Local D1 database (
solar-mining-db-dev), hot module replacement - Production: Production D1 database (
solar-mining-db), production Workers deployment
- Project Structure: Organized 3-worker architecture with proper separation of concerns
- TypeScript Configuration: Strict mode enabled with comprehensive type checking
- Build System: Vite configured with path aliases, code splitting, and optimization
- Testing Setup: Vitest configured with coverage reporting and test utilities
- Database Schema: Comprehensive schema with 8 core tables (miners, solar panels, storage, locations, etc.)
- Development Workflow: Complete npm scripts for all development tasks
- Code Quality: ESLint, Prettier, and Husky pre-commit hooks configured
- CI/CD Pipeline: GitHub Actions workflow with quality checks, builds, and deployment
- Documentation: Complete project documentation in docs/ directory
- Database Seeding: Populating equipment data with real ASIC and solar panel specifications
- Basic API Implementation: Equipment endpoints and system configuration CRUD operations
- External Data Integration: Bitcoin price, network data, and weather API integration
- Frontend Implementation: React components and pages (basic structure exists)
- Real-time Data Integration: Live Bitcoin network data, weather forecasting
- Advanced Calculations: Monte Carlo simulations, risk analysis, sensitivity testing
- Hardware Optimization: Equipment comparison and recommendation algorithms
- User Experience Enhancements: Configuration wizard, templates, export capabilities
- Environmental Modeling: Temperature coefficients, degradation factors, weather impact
The project uses strict TypeScript configuration with comprehensive type checking:
// tsconfig.json (actual configuration)
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyProperties": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true
}
}// Available path aliases
"@/*": ["src/*"]
"@/client/*": ["src/client/*"]
"@/server/*": ["src/server/*"]
"@/shared/*": ["src/shared/*"]
"@/components/*": ["src/client/components/*"]
"@/hooks/*": ["src/client/hooks/*"]
"@/services/*": ["src/client/services/*"]
"@/utils/*": ["src/client/utils/*"]
"@/types/*": ["src/shared/types/*"]- Use
interfacefor object shapes that might be extended - Use
typefor unions, intersections, and computed types - Prefer composition over inheritance
// ✅ Good: Composable interfaces
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface MinerModel extends BaseEntity {
name: string;
hashrate: number;
powerConsumption: number;
efficiency: number;
}
// ✅ Good: Union types for state
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };- Use descriptive generic names beyond
T - Constrain generics appropriately
- Leverage utility types for transformations
// ✅ Good: Descriptive generic names
interface ApiResponse<TData> {
success: boolean;
data: TData;
meta?: {
pagination?: PaginationMeta;
};
}
// ✅ Good: Generic constraints
interface Repository<TEntity extends BaseEntity> {
findById(id: TEntity['id']): Promise<TEntity | null>;
create(data: Omit<TEntity, keyof BaseEntity>): Promise<TEntity>;
update(id: TEntity['id'], data: Partial<TEntity>): Promise<TEntity>;
}- Use optional chaining (
?.) for potentially undefined properties - Prefer nullish coalescing (
??) over logical OR (||) - Use type guards for runtime type checking
// ✅ Good: Safe property access
const minerName = equipment?.miner?.name ?? 'Unknown Miner';
// ✅ Good: Type guards
function isValidMinerData(data: unknown): data is MinerModel {
return (
typeof data === 'object' &&
data !== null &&
'name' in data &&
'hashrate' in data &&
typeof (data as any).hashrate === 'number'
);
}// ✅ Good: Result pattern for error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
// ✅ Good: Discriminated unions for API responses
type ProjectionResult =
| { type: 'success'; projections: Projection[]; metadata: ProjectionMeta }
| { type: 'validation_error'; errors: ValidationError[] }
| { type: 'calculation_error'; message: string; code: string };The project uses a organized component architecture with clear separation of concerns:
src/client/components/
├── ui/ # Basic UI components (Button, Input, Card, etc.)
├── forms/ # Form components and form logic
├── charts/ # Data visualization components
└── layout/ # Layout components (Header, Sidebar, etc.)
- Step-by-step setup process for complex mining configurations
- Location & climate data integration
- Equipment selection and comparison
- Financial settings and optimization
- Side-by-side equipment comparison tables
- Performance charts and visualizations
- Equipment recommendation algorithms
- Real-time pricing integration
- PDF report generation
- Excel workbook exports
- CSV data exports
- Shareable calculation results
- Use functional components exclusively
- Leverage hooks for state and side effects
- Keep components focused on single responsibilities
// ✅ Good: Focused functional component
interface EquipmentCardProps {
equipment: MinerModel;
onSelect: (equipment: MinerModel) => void;
isSelected: boolean;
}
export function EquipmentCard({ equipment, onSelect, isSelected }: EquipmentCardProps) {
const handleClick = useCallback(() => {
onSelect(equipment);
}, [equipment, onSelect]);
return (
<div
className={`card ${isSelected ? 'selected' : ''}`}
onClick={handleClick}
>
<h3>{equipment.name}</h3>
<p>Hashrate: {equipment.hashrate} TH/s</p>
<p>Power: {equipment.powerConsumption}W</p>
</div>
);
}// ✅ Good: Reusable data fetching hook
function useEquipmentData() {
const [state, setState] = useState<AsyncState<MinerModel[]>>({ status: 'idle' });
const fetchEquipment = useCallback(async () => {
setState({ status: 'loading' });
try {
const response = await api.get<MinerModel[]>('/equipment');
setState({ status: 'success', data: response.data });
} catch (error) {
setState({
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}, []);
useEffect(() => {
fetchEquipment();
}, [fetchEquipment]);
return { ...state, refetch: fetchEquipment };
}// ✅ Good: Custom state management hook
function useProjectionCalculator() {
const [config, setConfig] = useState<ProjectionConfig>();
const [results, setResults] = useState<ProjectionResult[]>([]);
const [isCalculating, setIsCalculating] = useState(false);
const calculate = useCallback(async (newConfig: ProjectionConfig) => {
if (isCalculating) return;
setIsCalculating(true);
setConfig(newConfig);
try {
const projections = await calculateProjections(newConfig);
setResults(projections);
} catch (error) {
console.error('Calculation failed:', error);
setResults([]);
} finally {
setIsCalculating(false);
}
}, [isCalculating]);
return {
config,
results,
isCalculating,
calculate
};
}// ✅ Good: Compound component pattern
export function ProjectionCard({ children, ...props }: ProjectionCardProps) {
return <div className="projection-card" {...props}>{children}</div>;
}
ProjectionCard.Header = function Header({ title, subtitle }: HeaderProps) {
return (
<div className="projection-card-header">
<h3>{title}</h3>
{subtitle && <p>{subtitle}</p>}
</div>
);
};
ProjectionCard.Body = function Body({ children }: BodyProps) {
return <div className="projection-card-body">{children}</div>;
};
ProjectionCard.Footer = function Footer({ children }: FooterProps) {
return <div className="projection-card-footer">{children}</div>;
};- Use
useMemofor expensive calculations only - Use
useCallbackfor functions passed to child components - Use
React.memofor components that re-render frequently
// ✅ Good: Appropriate use of memoization
const ProjectionChart = React.memo(function ProjectionChart({ data, options }: Props) {
const chartData = useMemo(() => {
// Expensive transformation
return transformDataForChart(data);
}, [data]);
const handleDataPointClick = useCallback((point: DataPoint) => {
onDataPointClick?.(point);
}, [onDataPointClick]);
return <Chart data={chartData} onPointClick={handleDataPointClick} />;
});// ✅ Good: Modular handler structure
interface Env {
DB: D1Database;
API_KEY: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
const method = request.method;
try {
// Route handling
if (url.pathname.startsWith('/api/equipment')) {
return handleEquipmentRoutes(request, env, ctx);
}
if (url.pathname.startsWith('/api/projections')) {
return handleProjectionRoutes(request, env, ctx);
}
return new Response('Not found', { status: 404 });
} catch (error) {
return handleError(error);
}
},
} satisfies ExportedHandler<Env>;// ✅ Good: Request validation with Zod
import { z } from 'zod';
const ProjectionConfigSchema = z.object({
equipmentId: z.string(),
solarConfig: z.object({
panelCount: z.number().min(1),
panelWatts: z.number().min(1),
systemVoltage: z.number().min(12)
}),
projectionYears: z.number().min(1).max(10)
});
async function handleCreateProjection(request: Request, env: Env) {
const body = await request.json();
const result = ProjectionConfigSchema.safeParse(body);
if (!result.success) {
return Response.json({
error: 'Validation failed',
details: result.error.issues
}, { status: 400 });
}
const projection = await createProjection(result.data, env.DB);
return Response.json(projection);
}// ✅ Good: Centralized error handling
function handleError(error: unknown): Response {
console.error('Worker error:', error);
if (error instanceof ValidationError) {
return Response.json({
error: 'Validation Error',
message: error.message,
details: error.details
}, { status: 400 });
}
if (error instanceof DatabaseError) {
return Response.json({
error: 'Database Error',
message: 'Internal database error occurred'
}, { status: 500 });
}
return Response.json({
error: 'Internal Server Error',
message: 'An unexpected error occurred'
}, { status: 500 });
}// ✅ Good: Reuse database connections
class DatabaseService {
private constructor(private db: D1Database) {}
static create(db: D1Database): DatabaseService {
return new DatabaseService(db);
}
async findEquipment(): Promise<MinerModel[]> {
const { results } = await this.db
.prepare('SELECT * FROM equipment ORDER BY name')
.all();
return results as MinerModel[];
}
async createProjection(data: ProjectionData): Promise<Projection> {
const stmt = this.db.prepare(`
INSERT INTO projections (config, results, created_at)
VALUES (?1, ?2, ?3)
RETURNING *
`);
const result = await stmt
.bind(JSON.stringify(data.config), JSON.stringify(data.results), new Date().toISOString())
.first();
return result as Projection;
}
}-- Core tables in the current database schema
-- Complete schema is in src/server/shared/database/migrations/0001_initial_schema.sql
-- Equipment specifications
CREATE TABLE miner_models (
id INTEGER PRIMARY KEY,
manufacturer VARCHAR(50) NOT NULL,
model_name VARCHAR(100) NOT NULL,
hashrate_th REAL NOT NULL,
power_consumption_w INTEGER NOT NULL,
efficiency_j_th REAL NOT NULL,
-- Degradation and environmental factors
hashrate_degradation_annual REAL DEFAULT 0.05,
operating_temp_min INTEGER,
operating_temp_max INTEGER,
current_price_usd REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE solar_panel_models (
id INTEGER PRIMARY KEY,
manufacturer VARCHAR(50) NOT NULL,
model_name VARCHAR(100) NOT NULL,
rated_power_w INTEGER NOT NULL,
efficiency_percent REAL NOT NULL,
temperature_coefficient REAL NOT NULL,
degradation_rate_annual REAL DEFAULT 0.5,
cost_per_watt REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- System configurations and projections
CREATE TABLE system_configs (
id INTEGER PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
location_id INTEGER NOT NULL,
solar_panels JSON NOT NULL,
miners JSON NOT NULL,
electricity_rate_usd_kwh REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Market and environmental data tables
CREATE TABLE bitcoin_price_data (
id INTEGER PRIMARY KEY,
recorded_date DATE NOT NULL,
price_usd REAL NOT NULL,
data_source VARCHAR(50) NOT NULL
);
-- Complete schema includes 8 tables total
-- See migration file for full table definitions// ✅ Good: Prepared statements with proper error handling
export class EquipmentRepository {
constructor(private db: D1Database) {}
async findById(id: string): Promise<MinerModel | null> {
try {
const result = await this.db
.prepare('SELECT * FROM miners WHERE id = ?')
.bind(id)
.first();
return result as MinerModel | null;
} catch (error) {
throw new DatabaseError(`Failed to find miner ${id}`, { cause: error });
}
}
async findMostEfficient(limit = 10): Promise<MinerModel[]> {
try {
const { results } = await this.db
.prepare('SELECT * FROM miners ORDER BY efficiency_j_th ASC LIMIT ?')
.bind(limit)
.all();
return results as MinerModel[];
} catch (error) {
throw new DatabaseError('Failed to find efficient miners', { cause: error });
}
}
async create(data: CreateMinerData): Promise<MinerModel> {
try {
const id = generateId();
const now = new Date().toISOString();
const result = await this.db
.prepare(`
INSERT INTO miners (id, name, manufacturer, hashrate_th, power_consumption_w, efficiency_j_th, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
RETURNING *
`)
.bind(id, data.name, data.manufacturer, data.hashrate, data.powerConsumption, data.efficiency, now, now)
.first();
return result as MinerModel;
} catch (error) {
throw new DatabaseError('Failed to create miner', { cause: error });
}
}
}// ✅ Good: Version-controlled migrations
export interface Migration {
version: number;
name: string;
up: (db: D1Database) => Promise<void>;
down: (db: D1Database) => Promise<void>;
}
const migrations: Migration[] = [
{
version: 1,
name: 'create_miners_table',
async up(db) {
await db.exec(`
CREATE TABLE miners (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
hashrate_th REAL NOT NULL,
power_consumption_w INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
},
async down(db) {
await db.exec('DROP TABLE miners');
}
}
];
export async function runMigrations(db: D1Database) {
// Create migrations table if not exists
await db.exec(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Get current version
const current = await db.prepare('SELECT MAX(version) as version FROM migrations').first();
const currentVersion = (current?.version as number) || 0;
// Run pending migrations
for (const migration of migrations) {
if (migration.version > currentVersion) {
await migration.up(db);
await db.prepare('INSERT INTO migrations (version, name) VALUES (?, ?)')
.bind(migration.version, migration.name)
.run();
}
}
}The project is structured to support advanced calculations in the dedicated Calculation Worker:
src/server/calculations/
├── engines/ # Mathematical calculation engines
├── services/ # Calculation business logic
├── utils/ # Calculation utilities
└── index.ts # Calculation Worker entry point
- Solar power output: PV system modeling with irradiance data
- Mining hashrate: Effective hashrate with environmental factors
- Financial projections: ROI, payback period, operating costs
- Performance degradation: Equipment aging and efficiency decline
- Monte Carlo simulations: Risk analysis with confidence intervals
- Sensitivity analysis: Parameter impact assessment
- Hardware optimization: Multi-objective equipment selection
- Environmental modeling: Temperature and weather impact
interface SolarCalculationConfig {
panels: {
count: number;
watts_per_panel: number;
efficiency: number;
degradation_rate: number;
};
location: {
latitude: number;
longitude: number;
irradiance_data: number[];
};
environmental: {
temperature_coefficient: number;
soiling_losses: number;
system_losses: number;
};
}interface MiningCalculationConfig {
equipment: {
hashrate_th: number;
power_consumption_w: number;
efficiency_j_th: number;
degradation_rate: number;
};
network: {
difficulty: number;
block_reward: number;
btc_price_usd: number;
};
costs: {
electricity_rate_kwh: number;
maintenance_costs: number;
};
}interface FinancialProjection {
initial_investment: number;
monthly_revenue: number[];
monthly_costs: number[];
net_present_value: number;
internal_rate_of_return: number;
payback_period_months: number;
total_roi_percent: number;
}// vite.config.ts (actual current configuration)
export default defineConfig({
plugins: [react()],
esbuild: { target: 'es2022' },
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@/client': resolve(__dirname, 'src/client'),
'@/server': resolve(__dirname, 'src/server'),
'@/shared': resolve(__dirname, 'src/shared'),
'@/components': resolve(__dirname, 'src/client/components'),
'@/hooks': resolve(__dirname, 'src/client/hooks'),
'@/services': resolve(__dirname, 'src/client/services'),
'@/utils': resolve(__dirname, 'src/client/utils'),
'@/types': resolve(__dirname, 'src/shared/types')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8787',
changeOrigin: true
}
}
},
build: {
target: 'es2022',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
utils: ['date-fns', 'zod', 'clsx']
}
}
}
},
optimizeDeps: {
include: ['react', 'react-dom', 'react-router-dom', 'date-fns'],
esbuildOptions: { target: 'es2022' }
}
});// vite.config.production.ts
export default defineConfig({
...baseConfig,
build: {
...baseConfig.build,
minify: 'esbuild',
cssMinify: true,
reportCompressedSize: false, // Faster builds
chunkSizeWarningLimit: 1000
},
define: {
__DEV__: false,
__PROD__: true
}
});// ✅ Good: Route-based code splitting
const EquipmentPage = lazy(() => import('@/pages/EquipmentPage'));
const ProjectionsPage = lazy(() => import('@/pages/ProjectionsPage'));
const AnalyticsPage = lazy(() => import('@/pages/AnalyticsPage'));
function App() {
return (
<Router>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/equipment" element={<EquipmentPage />} />
<Route path="/projections" element={<ProjectionsPage />} />
<Route path="/analytics" element={<AnalyticsPage />} />
</Routes>
</Suspense>
</Router>
);
}- Use kebab-case for file names:
equipment-card.tsx - Use PascalCase for component files:
EquipmentCard.tsx - Use camelCase for utility files:
calculateProjections.ts - Use SCREAMING_SNAKE_CASE for constants:
API_ENDPOINTS.ts
components/
├── ui/ # Basic UI components
│ ├── Button.tsx
│ ├── Input.tsx
│ └── Card.tsx
├── forms/ # Form components
│ ├── EquipmentForm.tsx
│ └── ProjectionForm.tsx
├── charts/ # Visualization components
│ ├── ProjectionChart.tsx
│ └── EfficiencyChart.tsx
└── layout/ # Layout components
├── Header.tsx
├── Sidebar.tsx
└── PageLayout.tsx
// ✅ Good: Named exports for better tree-shaking
export function calculateProjections(config: ProjectionConfig): Promise<Projection[]> {
// Implementation
}
export function validateEquipment(equipment: MinerModel): boolean {
// Implementation
}
export const PROJECTION_DEFAULTS = {
years: 5,
degradationRate: 0.005
} as const;// index.ts - Barrel export file
export { calculateProjections, validateEquipment } from './calculations';
export { PROJECTION_DEFAULTS } from './constants';
export type { ProjectionConfig, Projection } from './types';// ✅ Good: Organized imports
// 1. Node modules
import React, { useState, useCallback, useMemo } from 'react';
import { z } from 'zod';
// 2. Internal modules (absolute imports)
import { EquipmentCard } from '@components/equipment/EquipmentCard';
import { useEquipmentData } from '@hooks/useEquipmentData';
import { calculateProjections } from '@services/calculations';
// 3. Relative imports
import './EquipmentList.css';
// 4. Type-only imports (separate)
import type { MinerModel, ProjectionConfig } from '@shared/types';// ✅ Good: Comprehensive component test
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import { EquipmentCard } from './EquipmentCard';
import type { MinerModel } from '@shared/types';
const mockMiner: MinerModel = {
id: 'test-1',
name: 'Test Miner S19',
manufacturer: 'Bitmain',
hashrate: 110,
powerConsumption: 3250,
efficiency: 29.5,
createdAt: new Date(),
updatedAt: new Date()
};
describe('EquipmentCard', () => {
const mockOnSelect = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it('displays miner information correctly', () => {
render(<EquipmentCard equipment={mockMiner} onSelect={mockOnSelect} isSelected={false} />);
expect(screen.getByText('Test Miner S19')).toBeInTheDocument();
expect(screen.getByText(/110 TH\/s/)).toBeInTheDocument();
expect(screen.getByText(/3250W/)).toBeInTheDocument();
});
it('calls onSelect when clicked', async () => {
render(<EquipmentCard equipment={mockMiner} onSelect={mockOnSelect} isSelected={false} />);
fireEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith(mockMiner);
});
});
it('applies selected styling when selected', () => {
render(<EquipmentCard equipment={mockMiner} onSelect={mockOnSelect} isSelected={true} />);
expect(screen.getByRole('button')).toHaveClass('selected');
});
});// ✅ Good: Service layer testing
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { EquipmentService } from './EquipmentService';
import type { D1Database } from '@cloudflare/workers-types';
// Mock D1 database
const mockDB = {
prepare: vi.fn(),
exec: vi.fn()
} as unknown as D1Database;
describe('EquipmentService', () => {
let service: EquipmentService;
beforeEach(() => {
vi.clearAllMocks();
service = new EquipmentService(mockDB);
});
describe('findById', () => {
it('returns miner when found', async () => {
const mockResult = { id: '1', name: 'Test Miner' };
const mockStatement = {
bind: vi.fn().mockReturnThis(),
first: vi.fn().mockResolvedValue(mockResult)
};
(mockDB.prepare as any).mockReturnValue(mockStatement);
const result = await service.findById('1');
expect(mockDB.prepare).toHaveBeenCalledWith('SELECT * FROM miners WHERE id = ?');
expect(mockStatement.bind).toHaveBeenCalledWith('1');
expect(result).toEqual(mockResult);
});
it('returns null when not found', async () => {
const mockStatement = {
bind: vi.fn().mockReturnThis(),
first: vi.fn().mockResolvedValue(null)
};
(mockDB.prepare as any).mockReturnValue(mockStatement);
const result = await service.findById('nonexistent');
expect(result).toBeNull();
});
});
});// ✅ Good: Integration test with real Worker
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { unstable_dev } from 'wrangler';
import type { UnstableDevWorker } from 'wrangler';
describe('Equipment API', () => {
let worker: UnstableDevWorker;
beforeAll(async () => {
worker = await unstable_dev('src/server/index.ts', {
experimental: { disableExperimentalWarning: true }
});
});
afterAll(async () => {
await worker.stop();
});
it('GET /api/equipment returns equipment list', async () => {
const response = await worker.fetch('/api/equipment');
const data = await response.json();
expect(response.status).toBe(200);
expect(Array.isArray(data)).toBe(true);
expect(data).toHaveLength.greaterThan(0);
});
it('POST /api/equipment creates new equipment', async () => {
const newEquipment = {
name: 'Test Miner',
manufacturer: 'Test Corp',
hashrate: 100,
powerConsumption: 3000,
efficiency: 30
};
const response = await worker.fetch('/api/equipment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newEquipment)
});
expect(response.status).toBe(201);
const created = await response.json();
expect(created).toMatchObject(newEquipment);
expect(created.id).toBeDefined();
});
});// ✅ Good: Custom error hierarchy
export abstract class AppError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
constructor(
message: string,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = this.constructor.name;
}
}
export class ValidationError extends AppError {
readonly code = 'VALIDATION_ERROR';
readonly statusCode = 400;
constructor(
message: string,
public readonly field: string,
public readonly value: unknown,
context?: Record<string, unknown>
) {
super(message, context);
}
}
export class DatabaseError extends AppError {
readonly code = 'DATABASE_ERROR';
readonly statusCode = 500;
}
export class CalculationError extends AppError {
readonly code = 'CALCULATION_ERROR';
readonly statusCode = 422;
}// ✅ Good: Error boundary component
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<PropsWithChildren, ErrorBoundaryState> {
constructor(props: PropsWithChildren) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error boundary caught error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.stack}</pre>
</details>
<button onClick={() => this.setState({ hasError: false, error: null })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}// ✅ Good: Memoized component with stable references
interface ProjectionListProps {
projections: Projection[];
onProjectionSelect: (projection: Projection) => void;
}
export const ProjectionList = React.memo(function ProjectionList({
projections,
onProjectionSelect
}: ProjectionListProps) {
// Stable callback reference
const handleSelect = useCallback((projection: Projection) => {
onProjectionSelect(projection);
}, [onProjectionSelect]);
// Memoize expensive calculations
const sortedProjections = useMemo(() => {
return [...projections].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}, [projections]);
return (
<div>
{sortedProjections.map(projection => (
<ProjectionCard
key={projection.id}
projection={projection}
onSelect={handleSelect}
/>
))}
</div>
);
});// ✅ Good: Virtual scrolling implementation
import { FixedSizeList as List } from 'react-window';
interface VirtualizedListProps {
items: MinerModel[];
onItemSelect: (item: MinerModel) => void;
}
export function VirtualizedEquipmentList({ items, onItemSelect }: VirtualizedListProps) {
const Row = useCallback(({ index, style }: ListChildComponentProps) => (
<div style={style}>
<EquipmentCard
equipment={items[index]}
onSelect={onItemSelect}
isSelected={false}
/>
</div>
), [items, onItemSelect]);
return (
<List
height={600}
itemCount={items.length}
itemSize={150}
width="100%"
>
{Row}
</List>
);
}// ✅ Good: Optimized queries with pagination
export class ProjectionRepository {
async findUserProjections(
userId: string,
options: PaginationOptions = {}
): Promise<PaginatedResult<Projection>> {
const { offset = 0, limit = 20, sortBy = 'created_at', sortOrder = 'DESC' } = options;
// Use prepared statement with proper indexing
const query = `
SELECT p.*, m.name as miner_name
FROM projections p
JOIN miners m ON p.miner_id = m.id
WHERE p.user_id = ?
ORDER BY p.${sortBy} ${sortOrder}
LIMIT ? OFFSET ?
`;
const [projections, total] = await Promise.all([
this.db.prepare(query).bind(userId, limit, offset).all(),
this.db.prepare('SELECT COUNT(*) as count FROM projections WHERE user_id = ?')
.bind(userId).first()
]);
return {
data: projections.results as Projection[],
total: (total as any).count,
offset,
limit,
hasMore: offset + limit < (total as any).count
};
}
}- Feature branches:
feature/equipment-management - Bug fixes:
fix/calculation-error - Hotfixes:
hotfix/critical-security-patch
feat: add solar panel efficiency calculator
- Implement PV system modeling calculations
- Add degradation factor calculations
- Include temperature coefficient adjustments
Closes #123
// package.json
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run type-check && npm run test"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{md,json}": [
"prettier --write"
]
}
}- All tests pass (
npm run test) - Type checking passes (
npm run type-check) - Linting passes (
npm run lint) - Code is properly formatted (
npm run format) - Documentation is updated
- Breaking changes are noted
- Code follows established patterns
- Error handling is appropriate
- Performance considerations are addressed
- Security best practices are followed
- Tests provide adequate coverage
- Types are properly defined
# Development (Multi-Worker Architecture)
npm run dev # Start all services (client + all workers)
npm run dev:client # Start frontend only (port 3000)
npm run dev:api # Start API Worker only (port 8787)
npm run dev:calculations # Start Calculation Worker only (port 8788)
npm run dev:data # Start Data Worker only (port 8789)
# Building
npm run build # Build all components
npm run build:client # Build frontend only
npm run build:workers # Build all workers
npm run build:api # Build API Worker only
npm run build:calculations # Build Calculation Worker only
npm run build:data # Build Data Worker only
# Testing
npm run test # Run tests in watch mode
npm run test:ci # Run tests with coverage (CI mode)
npm run test:coverage # Run tests with verbose coverage
npm run test:unit # Run unit tests only
npm run test:integration # Run integration tests only
npm run test:smoke # Run smoke tests
# Code Quality
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint issues automatically
npm run lint:ci # Run ESLint with JUnit output for CI
npm run format # Format code with Prettier
npm run format:check # Check code formatting
npm run type-check # Run TypeScript compiler checks
# Database Management
npm run db:migrate # Run database migrations (development)
npm run db:migrate:production # Run database migrations (production)
npm run db:seed # Seed database with sample data
npm run db:reset # Reset and reseed database
# Deployment
npm run deploy # Deploy all workers to production
npm run deploy:dev # Deploy all workers to development
npm run deploy:api # Deploy API Worker to production
npm run deploy:calculations # Deploy Calculation Worker to production
npm run deploy:data # Deploy Data Worker to production
npm run pages:deploy # Deploy frontend to Cloudflare Pages
# Monitoring
npm run logs:api # View API Worker logs
npm run logs:calculations # View Calculation Worker logs
npm run logs:data # View Data Worker logs- ✅ Project architecture and configuration setup
- 🔄 Basic frontend components and pages
- 🔄 API endpoints and database integration
- 🔄 Basic mining profitability calculations
- 📋 Equipment database integration
- 📋 Real-time data integration (Bitcoin network, weather)
- 📋 Monte Carlo risk analysis
- 📋 Hardware optimization algorithms
- 📋 Advanced environmental modeling
- 📋 Configuration wizard and templates
- 📋 Interactive charts and visualizations
- 📋 Export capabilities (PDF, Excel, CSV)
- 📋 Hardware comparison tools
- 📋 Real-time monitoring dashboard
- 📋 Mobile-responsive design optimization
- 📋 Multi-user support and authentication
- 📋 Portfolio management for multiple projects
- 📋 Advanced reporting and analytics
- 📋 API access for third-party integrations
- 📋 White-label solutions
The project has a complete GitHub Actions workflow at .github/workflows/ci-cd.yml with the following pipeline:
- TypeScript type checking: Ensures all types are valid
- ESLint code quality: Checks code standards and best practices
- Prettier formatting: Validates consistent code formatting
- Test coverage: Runs all tests with coverage reporting
- Claude Code Analysis: AI-powered code review on pull requests
- Client build: Compiles React frontend for production
- Worker build: Validates all 3 Cloudflare Workers (dry-run)
- Artifact storage: Saves build outputs for deployment
- Triggers: Automatic on pull requests and non-main branches
- Multi-worker deployment: Deploys all 3 workers to development environment
- Frontend deployment: Deploys to Cloudflare Pages staging
- Triggers: Automatic on main branch push (with approval)
- Environment: Production environment with URL: https://solar-mining-calculator.pages.dev
- Multi-worker deployment: Deploys all 3 workers to production
- Frontend deployment: Deploys to production Cloudflare Pages
# Build and deploy all workers
npm run build
npm run deploy
# Deploy frontend to Cloudflare Pages
npm run pages:deployThis development guide should be referenced for all code contributions and maintained as the project evolves. Regular updates ensure alignment with best practices and emerging patterns in the ecosystem.