7 Commits

Author SHA1 Message Date
Misaka
f112046178 feat: implement Winston logging service
- Add LogEntry interface for structured log data
- Create LoggerService with Winston integration
- Support for console and file transports (app.log, error.log)
- Implement UI log notification callbacks
- Provide info, warn, error, and debug logging methods
- Configure log rotation (10MB max, 5 files retained)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 22:17:09 +08:00
Misaka
319b5ec03b feat: implement configuration management system
- Add comprehensive TypeScript interfaces for all config types
- Implement ConfigManager singleton class with layered loading
- Support for default, environment-specific, and environment variable configs
- Add example .env file for sensitive configuration

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 22:17:09 +08:00
Misaka
ae60273782 feat: implement configuration management system
- Add comprehensive TypeScript interfaces for all config types
- Implement ConfigManager singleton class with layered loading
- Support for default, environment-specific, and environment variable configs
- Add example .env file for sensitive configuration

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 22:10:41 +08:00
Misaka
fb6f2bbc02 feat: configure TypeScript with strict mode 2026-02-28 22:07:52 +08:00
Misaka
cacc53a184 feat: install core dependencies (playwright, database, logging)
Install runtime dependencies:
- playwright: For browser automation
- mssql: SQL Server database connectivity
- mysql2: MySQL database connectivity
- winston: Logging framework
- exceljs: Excel file generation and manipulation

Install dev dependencies:
- @types/node: TypeScript definitions for Node.js

Update package.json scripts:
- postinstall: Automatically install Playwright Chromium
- test: Jest unit test runner
- test:e2e: Playwright end-to-end test runner

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 22:01:03 +08:00
Misaka
b5a8c11a30 fix: restore Electron template files deleted in Task 1
This commit restores essential Electron + React template files that were
accidentally deleted during Task 1 implementation. These files were part
of the initial template setup (commit 0077005) and are required for the
Electron application to function properly.

Restored files:
- src/preload/index.d.ts
- src/preload/index.ts
- src/renderer/index.html
- src/renderer/src/assets/base.css
- src/renderer/src/assets/electron.svg
- src/renderer/src/assets/main.css
- src/renderer/src/assets/wavy-lines.svg
- src/renderer/src/components/Versions.tsx
- src/renderer/src/env.d.ts
- src/renderer/src/main.tsx
- src/renderer/src/App.tsx (restored to original template)

The directory structure additions from Task 1 (hooks, pages, styles) are
preserved.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 21:44:06 +08:00
Misaka
7704c0c067 feat: create project directory structure
- Create main process structure (services, dao, controllers, models, utils, config)
- Create preload script directory with .gitkeep
- Create renderer structure (pages, components, hooks, styles)
- Add placeholder App.tsx for React (to be implemented in later tasks)
- Create config, tests, logs directories
- Create data directories (temp, output, reports)
- Update .gitignore to exclude logs, data files, and environment configs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-28 21:39:52 +08:00
24 changed files with 12366 additions and 6 deletions

18
.gitignore vendored
View File

@@ -6,4 +6,20 @@ out
*.log*
#AI Agent
.claude
.claude
# Logs
logs/
*.log
# Data
data/temp/*
data/output/*
data/reports/*
!data/temp/.gitkeep
!data/output/.gitkeep
!data/reports/.gitkeep
# Environment
config/.env
config/.env.local

18
config/.env.example Normal file
View File

@@ -0,0 +1,18 @@
# SQL Server
SQL_SERVER_SERVER=192.168.110.114
SQL_SERVER_DATABASE=CompanyDB
SQL_SERVER_USERNAME=peng
SQL_SERVER_PASSWORD=your_password_here
SQL_SERVER_DRIVER=ODBC Driver 18 for SQL Server
SQL_SERVER_TRUST_CERT=yes
# MySQL
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=erp_db
MYSQL_USERNAME=root
MYSQL_PASSWORD=your_password_here
# ERP Credentials (optional, can also be entered in UI)
ERP_USERNAME=BLDpengqiangqiang
ERP_PASSWORD=your_password_here

0
config/.gitkeep Normal file
View File

19
config/app.json Normal file
View File

@@ -0,0 +1,19 @@
{
"appName": "ERPAuto",
"version": "1.0.0",
"logLevel": "info",
"browser": {
"headless": false,
"slowMo": 50,
"timeout": 30000
},
"erp": {
"baseUrl": "",
"ignoreHttpsErrors": false
},
"paths": {
"tempDir": "./data/temp",
"outputDir": "./data/output",
"reportDir": "./data/reports"
}
}

7
config/development.json Normal file
View File

@@ -0,0 +1,7 @@
{
"logLevel": "debug",
"browser": {
"headless": false,
"slowMo": 100
}
}

0
data/output/.gitkeep Normal file
View File

0
data/reports/.gitkeep Normal file
View File

0
data/temp/.gitkeep Normal file
View File

0
logs/.gitkeep Normal file
View File

11981
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,15 +14,22 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"postinstall": "electron-builder install-app-deps && playwright install chromium",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
"build:linux": "electron-vite build && electron-builder --linux",
"test": "jest",
"test:e2e": "playwright test"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0"
"@electron-toolkit/utils": "^4.0.0",
"exceljs": "^4.4.0",
"mssql": "^12.2.0",
"mysql2": "^3.18.2",
"playwright": "^1.58.2",
"winston": "^3.19.0"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^3.0.0",

0
src/main/config/.gitkeep Normal file
View File

View File

@@ -0,0 +1,158 @@
import fs from 'fs';
import path from 'path';
import { AppConfig } from '../models/config.types';
export class ConfigManager {
private static instance: AppConfig | null = null;
static load(): AppConfig {
if (this.instance) {
return this.instance;
}
try {
// Load default config (minimal, no sensitive data)
const defaultConfig = this.getDefaultConfig();
// Load environment-specific config
const env = process.env.NODE_ENV || 'development';
const envConfigPath = path.join(__dirname, `../../../config/${env}.json`);
let envConfig: Partial<AppConfig> = {};
if (fs.existsSync(envConfigPath)) {
try {
const envConfigContent = fs.readFileSync(envConfigPath, 'utf-8');
envConfig = JSON.parse(envConfigContent);
} catch (error) {
console.warn(`Failed to load env config from ${envConfigPath}:`, error);
}
}
// Load environment variables (with validation)
const envVars = this.loadFromEnv();
// Deep merge configurations
this.instance = this.deepMerge(defaultConfig, envConfig, envVars);
// Validate required configuration
this.validateConfig(this.instance);
return this.instance;
} catch (error) {
console.error('Failed to load configuration:', error);
throw new Error('Configuration loading failed');
}
}
private static getDefaultConfig(): AppConfig {
const appConfigPath = path.join(__dirname, '../../../config/app.json');
try {
if (fs.existsSync(appConfigPath)) {
const configContent = fs.readFileSync(appConfigPath, 'utf-8');
return JSON.parse(configContent);
}
} catch (error) {
console.warn(`Failed to load app.json, using minimal defaults:`, error);
}
// Minimal fallback defaults (NO sensitive data)
return {
appName: 'ERPAuto',
version: '1.0.0',
logLevel: 'info',
browser: {
headless: false,
slowMo: 50,
timeout: 30000,
},
databases: {
sqlServer: {
server: '', // MUST be set via env var
database: '',
username: '',
password: '',
driver: 'ODBC Driver 18 for SQL Server',
trustServerCertificate: 'yes',
},
mysql: {
host: 'localhost',
port: 3306,
database: '',
username: '',
password: '',
},
},
erp: {
baseUrl: '', // MUST be set via env var
ignoreHttpsErrors: false,
},
paths: {
tempDir: './data/temp',
outputDir: './data/output',
reportDir: './data/reports',
},
};
}
private static loadFromEnv(): Partial<AppConfig> {
const sqlServer = {
server: process.env.SQL_SERVER_SERVER || '',
database: process.env.SQL_SERVER_DATABASE || '',
username: process.env.SQL_SERVER_USERNAME || '',
password: process.env.SQL_SERVER_PASSWORD || '',
driver: process.env.SQL_SERVER_DRIVER || 'ODBC Driver 18 for SQL Server',
trustServerCertificate: process.env.SQL_SERVER_TRUST_CERT || 'yes',
};
const mysql = {
host: process.env.MYSQL_HOST || 'localhost',
port: parseInt(process.env.MYSQL_PORT || '3306'),
database: process.env.MYSQL_DATABASE || '',
username: process.env.MYSQL_USERNAME || '',
password: process.env.MYSQL_PASSWORD || '',
};
return {
databases: {
sqlServer,
mysql,
},
};
}
private static deepMerge(...configs: Partial<AppConfig>[]): AppConfig {
const result = configs[0] as AppConfig;
for (let i = 1; i < configs.length; i++) {
const config = configs[i];
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const value = (config as any)[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
(result as any)[key] = { ...(result as any)[key], ...value };
} else {
(result as any)[key] = value;
}
}
}
}
return result;
}
private static validateConfig(config: AppConfig): void {
// Validate critical configuration
if (!config.databases.sqlServer.server && process.env.NODE_ENV === 'production') {
throw new Error('SQL Server server address must be configured via environment variable');
}
if (!config.databases.sqlServer.database) {
throw new Error('SQL Server database name must be configured');
}
if (!config.erp.baseUrl) {
console.warn('Warning: ERP base URL not configured, ERP features will not work');
}
}
}

View File

0
src/main/dao/.gitkeep Normal file
View File

0
src/main/models/.gitkeep Normal file
View File

View File

@@ -0,0 +1,46 @@
export interface SQLServerConfig {
server: string;
database: string;
username: string;
password: string;
driver: string;
trustServerCertificate?: string;
}
export interface MySQLConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
}
export interface BrowserConfig {
headless: boolean;
slowMo: number;
timeout: number;
}
export interface ERPConfig {
baseUrl: string;
ignoreHttpsErrors: boolean;
}
export interface PathConfig {
tempDir: string;
outputDir: string;
reportDir: string;
}
export interface AppConfig {
appName: string;
version: string;
logLevel: 'debug' | 'info' | 'warn' | 'error';
browser: BrowserConfig;
databases: {
sqlServer: SQLServerConfig;
mysql: MySQLConfig;
};
erp: ERPConfig;
paths: PathConfig;
}

View File

@@ -0,0 +1,6 @@
export interface LogEntry {
timestamp: string;
level: string;
message: string;
details?: any;
}

View File

View File

@@ -0,0 +1,94 @@
import winston from 'winston';
import path from 'path';
import { app } from 'electron';
import { LogEntry } from '../models/logger.types';
export class LoggerService {
private static instance: winston.Logger | null = null;
private static uiLogCallbacks: Set<(logEntry: LogEntry) => void> = new Set();
static initialize(): winston.Logger {
if (this.instance) {
return this.instance;
}
const logDir = path.join(app.getPath('userData'), 'logs');
this.instance = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ level, message, timestamp, stack }) => {
if (stack) {
return `[${timestamp}] [${level.toUpperCase()}] ${message}\n${stack}`;
}
return `[${timestamp}] [${level.toUpperCase()}] ${message}`;
})
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}),
new winston.transports.File({
filename: path.join(logDir, 'app.log'),
maxsize: 10 * 1024 * 1024,
maxFiles: 5,
}),
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 10 * 1024 * 1024,
maxFiles: 5,
}),
],
});
return this.instance;
}
static onUILog(callback: (logEntry: LogEntry) => void): () => void {
this.uiLogCallbacks.add(callback);
return () => {
this.uiLogCallbacks.delete(callback);
};
}
private static notifyUI(level: string, message: string, details?: any): void {
const logEntry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
details,
};
this.uiLogCallbacks.forEach((callback) => callback(logEntry));
}
static info(message: string, details?: any): void {
if (!this.instance) this.initialize();
this.instance!.info(message, details);
this.notifyUI('info', message, details);
}
static warn(message: string, details?: any): void {
if (!this.instance) this.initialize();
this.instance!.warning(message, details);
this.notifyUI('warn', message, details);
}
static error(message: string, details?: any): void {
if (!this.instance) this.initialize();
this.instance!.error(message, details);
this.notifyUI('error', message, details);
}
static debug(message: string, details?: any): void {
if (!this.instance) this.initialize();
this.instance!.debug(message, details);
this.notifyUI('debug', message, details);
}
}

0
src/main/utils/.gitkeep Normal file
View File

0
src/preload/.gitkeep Normal file
View File

0
tests/.gitkeep Normal file
View File

View File

@@ -1,4 +1,12 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "tests"]
}