diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d5eaae7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,58 @@
+# Dependencies
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Build outputs
+dist/
+.tmp/
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# IDE files
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Logs
+logs
+*.log
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage/
+
+# nyc test coverage
+.nyc_output
+
+# Dependency directories
+jspm_packages/
+
+# Optional npm cache directory
+.npm
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
\ No newline at end of file
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 0000000..f741e01
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,136 @@
+# Development Guide
+
+## Quick Start
+
+```bash
+# Install dependencies
+npm install
+
+# Start development with auto-reload
+npm run dev:full
+```
+
+Open `https://localhost:8443` in your browser (accept the SSL certificate warning).
+
+## Available Scripts
+
+| Script | Description |
+| --------------------- | --------------------------------------------------------- |
+| `npm run build` | Build for development (with source maps) |
+| `npm run build:prod` | Build for production (minified, optimized) |
+| `npm run clean` | Clean the dist directory |
+| `npm run serve:https` | Serve built app over HTTPS (required for WebHID) |
+| `npm run serve` | Serve built app over HTTP (WebHID won't work) |
+| `npm run start` | Build and serve over HTTPS |
+| `npm run dev:full` | **Recommended**: Build, watch, and serve with auto-reload |
+| `npm run watch` | Watch files and rebuild on changes |
+
+## Development Workflow
+
+### For Active Development
+
+```bash
+npm run dev:full
+```
+
+This starts the complete development environment:
+
+- Builds the application
+- Watches for file changes
+- Serves over HTTPS at `https://localhost:8443`
+- Automatically rebuilds when you save files
+
+### For Testing Built Version
+
+```bash
+npm run start
+```
+
+This builds once and serves the result.
+
+## Important Notes
+
+### HTTPS Requirement
+
+The WebHID API requires HTTPS. The development server uses self-signed certificates located at:
+
+- `server.crt` - SSL certificate
+- `server.key` - SSL private key
+
+### Browser Compatibility
+
+- **Chrome/Edge**: Full WebHID support ✅
+- **Firefox**: No WebHID support ❌
+- **Safari**: No WebHID support ❌
+
+### SSL Certificate Warning
+
+When first accessing `https://localhost:8443`, your browser will show a security warning because we're using a self-signed certificate. This is normal for development - click "Advanced" and "Proceed to localhost" to continue.
+
+## File Structure
+
+```
+├── js/ # Source JavaScript files
+│ ├── core.js # Main application entry point
+│ ├── controllers/ # Controller-specific classes
+│ └── modals/ # Modal dialog handlers
+├── css/ # Source CSS files
+├── templates/ # HTML template files
+├── lang/ # Translation JSON files
+├── assets/ # SVG assets
+├── dist/ # Built application (auto-generated)
+└── dev-server.js # Custom development server
+```
+
+## Build Process
+
+The build system uses Gulp with the following steps:
+
+1. **JavaScript**: Bundled with Rollup, supports ES modules
+2. **CSS**: Concatenated and optionally minified
+3. **HTML**: Processed and optionally minified
+4. **Assets**: Copied to dist, SVGs can be inlined in production
+5. **Languages**: JSON files copied and optionally minified
+
+### Development vs Production
+
+| Feature | Development | Production |
+| -------------- | ----------- | ---------- |
+| Source maps | ✅ | ❌ |
+| Minification | ❌ | ✅ |
+| Asset inlining | ❌ | ✅ |
+| File hashing | ❌ | ✅ |
+
+## Troubleshooting
+
+### Port Already in Use
+
+If port 8443 is busy:
+
+```bash
+PORT=8444 npm run serve:https
+```
+
+### Build Errors
+
+Clean and rebuild:
+
+```bash
+npm run clean
+npm run build
+```
+
+### SSL Certificate Issues
+
+The certificates are pre-generated. If you need new ones:
+
+```bash
+openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/CN=localhost"
+```
+
+### WebHID Not Working
+
+1. Make sure you're using HTTPS (`npm run serve:https`)
+2. Use Chrome or Edge browser
+3. Accept the SSL certificate warning
+4. Check browser console for errors
diff --git a/README.md b/README.md
index 9603137..99f3bd3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,93 @@
-# dualshock-tools.github.io
+# DualShock Calibration GUI
-The code behind the DualShock Calibration GUI
+A web-based calibration tool for PlayStation DualShock 4, DualSense, and DualSense Edge controllers using the WebHID API.
+
+## Features
+
+- Controller connection via WebHID API
+- Stick calibration and range calibration
+- Input testing and visualization
+- Battery status display
+- Multi-language support (20+ languages)
+- Progressive Web App capabilities
+
+## Development
+
+### Prerequisites
+
+- Node.js (v16 or higher)
+- npm or yarn
+- Modern browser with WebHID support (Chrome/Edge)
+
+### Getting Started
+
+1. **Install dependencies:**
+
+ ```bash
+ npm install
+ ```
+
+2. **Build the application:**
+
+ ```bash
+ npm run build
+ ```
+
+3. **Start the development server:**
+
+ ```bash
+ npm run start
+ ```
+
+ The app will be available at `https://localhost:8443`
+
+### Development Scripts
+
+- `npm run build` - Build the application for development
+- `npm run build:prod` - Build the application for production
+- `npm run clean` - Clean the dist directory
+- `npm run serve:https` - Serve the built app over HTTPS (required for WebHID)
+- `npm run serve` - Serve the built app over HTTP (WebHID won't work)
+- `npm run start` - Build and serve the app
+- `npm run dev:full` - Build, watch for changes, and serve with auto-reload
+- `npm run watch` - Watch for file changes and rebuild
+
+### Development Workflow
+
+For active development with auto-rebuild:
+
+```bash
+npm run dev:full
+```
+
+This will:
+
+1. Build the application
+2. Start watching for file changes
+3. Serve the app over HTTPS at `https://localhost:8443`
+4. Automatically rebuild when files change
+
+### Important Notes
+
+- **HTTPS Required**: The WebHID API requires HTTPS. The development server uses self-signed certificates.
+- **Browser Security**: You may need to accept the self-signed certificate warning in your browser.
+- **Controller Support**: Only works in browsers with WebHID support (Chrome, Edge, Opera).
+
+### Project Structure
+
+- `js/` - Source JavaScript files
+- `css/` - Source CSS files
+- `templates/` - HTML template files
+- `lang/` - Translation files
+- `assets/` - SVG assets
+- `dist/` - Built application (generated)
+
+### Build System
+
+The project uses Gulp for building:
+
+- JavaScript bundling with Rollup
+- CSS concatenation and minification
+- HTML processing and minification
+- Asset optimization
+- Development vs production builds
diff --git a/dev-server.js b/dev-server.js
new file mode 100644
index 0000000..8665830
--- /dev/null
+++ b/dev-server.js
@@ -0,0 +1,183 @@
+#!/usr/bin/env node
+
+import https from 'https';
+import http from 'http';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Configuration
+const config = {
+ port: process.env.PORT || 8443,
+ httpPort: process.env.HTTP_PORT || 8080,
+ host: process.env.HOST || 'localhost',
+ distDir: path.join(__dirname, 'dist'),
+ certFile: path.join(__dirname, 'server.crt'),
+ keyFile: path.join(__dirname, 'server.key'),
+ useHttps: process.env.HTTPS === 'true'
+};
+
+// MIME types
+const mimeTypes = {
+ '.html': 'text/html',
+ '.js': 'text/javascript',
+ '.css': 'text/css',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.gif': 'image/gif',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.webmanifest': 'application/manifest+json'
+};
+
+function getMimeType(filePath) {
+ const ext = path.extname(filePath).toLowerCase();
+ return mimeTypes[ext] || 'application/octet-stream';
+}
+
+function requestHandler(req, res) {
+ // Parse URL and remove query parameters
+ let urlPath = new URL(req.url, `http://${req.headers.host}`).pathname;
+
+ // Default to index.html for root requests
+ if (urlPath === '/') {
+ urlPath = '/index.html';
+ }
+
+ const filePath = path.join(config.distDir, urlPath);
+ const mimeType = getMimeType(filePath);
+
+ // Security check - ensure file is within dist directory
+ if (!filePath.startsWith(config.distDir)) {
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
+ res.end('Forbidden');
+ return;
+ }
+
+ // Set CORS headers for development
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ // Disable caching for development
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+
+ // Handle OPTIONS requests
+ if (req.method === 'OPTIONS') {
+ res.writeHead(200);
+ res.end();
+ return;
+ }
+
+ fs.readFile(filePath, (err, data) => {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ // Try to serve index.html for SPA routing
+ const indexPath = path.join(config.distDir, 'index.html');
+ fs.readFile(indexPath, (indexErr, indexData) => {
+ if (indexErr) {
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
+ res.end('Not Found');
+ } else {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(indexData);
+ }
+ });
+ } else {
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
+ res.end('Internal Server Error');
+ }
+ } else {
+ res.writeHead(200, { 'Content-Type': mimeType });
+ res.end(data);
+ }
+ });
+}
+
+function startServer() {
+ // Check if dist directory exists
+ if (!fs.existsSync(config.distDir)) {
+ console.error(`❌ Dist directory not found: ${config.distDir}`);
+ console.log('💡 Run "npm run build" first to build the application');
+ process.exit(1);
+ }
+
+ if (config.useHttps) {
+ // Check if SSL certificates exist
+ if (!fs.existsSync(config.certFile) || !fs.existsSync(config.keyFile)) {
+ console.error('❌ SSL certificates not found');
+ console.log('💡 SSL certificates are required for WebHID API');
+ console.log(' Make sure server.crt and server.key exist in the project root');
+ process.exit(1);
+ }
+
+ // Read SSL certificates
+ const options = {
+ key: fs.readFileSync(config.keyFile),
+ cert: fs.readFileSync(config.certFile)
+ };
+
+ // Create HTTPS server
+ const server = https.createServer(options, requestHandler);
+
+ server.listen(config.port, config.host, () => {
+ console.log('🚀 Development server started!');
+ console.log(`📱 App running at: https://${config.host}:${config.port}`);
+ console.log('🔒 HTTPS enabled (required for WebHID API)');
+ console.log('💡 Press Ctrl+C to stop the server');
+ console.log('');
+ console.log('📝 Note: You may need to accept the self-signed certificate in your browser');
+ });
+
+ server.on('error', (err) => {
+ if (err.code === 'EADDRINUSE') {
+ console.error(`❌ Port ${config.port} is already in use`);
+ console.log('💡 Try using a different port: PORT=8444 npm run serve:https');
+ } else {
+ console.error('❌ Server error:', err.message);
+ }
+ process.exit(1);
+ });
+ } else {
+ // Create HTTP server (for testing only - WebHID won't work)
+ const server = http.createServer(requestHandler);
+
+ server.listen(config.httpPort, config.host, () => {
+ console.log('🚀 Development server started!');
+ console.log(`📱 App running at: http://${config.host}:${config.httpPort}`);
+ console.log('⚠️ HTTP mode - WebHID API will only work on localhost');
+ console.log('💡 Use "npm run serve:https" to enable WebHID support to other clients on the local network');
+ console.log('💡 Press Ctrl+C to stop the server');
+ });
+
+ server.on('error', (err) => {
+ if (err.code === 'EADDRINUSE') {
+ console.error(`❌ Port ${config.httpPort} is already in use`);
+ console.log('💡 Try using a different port: HTTP_PORT=8081 npm run serve');
+ } else {
+ console.error('❌ Server error:', err.message);
+ }
+ process.exit(1);
+ });
+ }
+}
+
+// Handle graceful shutdown
+process.on('SIGINT', () => {
+ console.log('\n👋 Shutting down development server...');
+ process.exit(0);
+});
+
+process.on('SIGTERM', () => {
+ console.log('\n👋 Shutting down development server...');
+ process.exit(0);
+});
+
+// Start the server
+startServer();
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..e2fcb02
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,318 @@
+import gulp from 'gulp';
+import cleanCSS from 'gulp-clean-css';
+import htmlmin from 'gulp-htmlmin';
+import concat from 'gulp-concat';
+import sourcemaps from 'gulp-sourcemaps';
+import gulpif from 'gulp-if';
+import rename from 'gulp-rename';
+import { deleteAsync } from 'del';
+import yargs from 'yargs';
+import { hideBin } from 'yargs/helpers';
+import replace from 'gulp-replace';
+import jsonMinify from 'gulp-json-minify';
+import crypto from 'crypto';
+import { rollup } from 'rollup';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import rollupTerser from '@rollup/plugin-terser';
+import fs from 'fs/promises';
+import path from 'path';
+import { glob } from 'glob';
+
+// Get command line arguments
+const argv = yargs(hideBin(process.argv)).argv;
+const isProduction = argv.production || process.env.NODE_ENV === 'production';
+
+// Paths configuration
+const paths = {
+ src: {
+ js: {
+ entry: 'js/core.js',
+ all: 'js/**/*.js'
+ },
+ css: ['css/main.css', 'css/finetune.css'],
+ html: {
+ main: 'index.html',
+ templates: 'templates/**/*.html'
+ },
+ lang: 'lang/**/*.json',
+ assets: [
+ 'favicon.ico',
+ 'favicon.svg',
+ 'favicon-16x16.png',
+ 'favicon-32x32.png',
+ 'favicon-96x96.png',
+ 'apple-touch-icon.png',
+ 'web-app-manifest-192x192.png',
+ 'web-app-manifest-512x512.png',
+ 'site.webmanifest',
+ 'donate.png',
+ 'googlec4c2e36a49e62fa3.html',
+ 'fa.min.css'
+ ],
+ svg: 'assets/**/*.svg'
+ },
+ dist: 'dist',
+ temp: '.tmp'
+};
+
+// Clean task
+function clean() {
+ return deleteAsync([paths.dist, paths.temp]);
+}
+
+// JavaScript bundling with Rollup
+async function scripts() {
+ const inputOptions = {
+ input: paths.src.js.entry,
+ plugins: [
+ nodeResolve(),
+ ...(isProduction ? [
+ rollupTerser({
+ compress: {
+ drop_console: false,
+ drop_debugger: true,
+ pure_funcs: ['console.debug', 'console.trace'],
+ passes: 2,
+ unsafe: true,
+ unsafe_comps: true,
+ unsafe_math: true,
+ unsafe_proto: true
+ },
+ mangle: {
+ reserved: [
+ 'gboot', 'connect', 'disconnect', 'disconnectSync',
+ 'show_faq_modal', 'calibrate_stick_centers', 'calibrate_range',
+ 'ds5_finetune', 'auto_calibrate_stick_centers', 'flash_all_changes',
+ 'reboot_controller', 'welcome_accepted', 'show_info_tab',
+ 'nvslock', 'nvsunlock', 'refresh_nvstatus', 'show_edge_modal',
+ 'show_donate_modal'
+ ],
+ properties: {
+ regex: /^_/
+ }
+ },
+ format: {
+ comments: false
+ }
+ })
+ ] : [])
+ ]
+ };
+
+ let filename = 'app.js';
+
+ if (isProduction) {
+ const bundle = await rollup(inputOptions);
+ const { output } = await bundle.generate({ format: 'es' });
+ const code = output[0].code;
+ const hash = crypto.createHash('md5').update(code).digest('hex').substring(0, 8);
+ filename = `app-${hash}.js`;
+
+ await fs.writeFile(path.join(paths.dist, filename), code);
+ await bundle.close();
+ } else {
+ const outputOptions = {
+ file: path.join(paths.dist, filename),
+ format: 'es',
+ sourcemap: true
+ };
+
+ const bundle = await rollup(inputOptions);
+ await bundle.write(outputOptions);
+ await bundle.close();
+ }
+
+ // Store the filename for HTML processing
+ global.jsFilename = filename;
+ return Promise.resolve();
+}
+
+// CSS processing
+function styles() {
+ let stream = gulp.src(paths.src.css)
+ .pipe(gulpif(!isProduction, sourcemaps.init()))
+ .pipe(concat('app.css'));
+
+ if (isProduction) {
+ stream = stream.pipe(cleanCSS({
+ level: 2
+ }));
+
+ // Add hash to filename in production
+ stream = stream.pipe(rename(function(path) {
+ const hash = crypto.createHash('md5').update(Date.now().toString()).digest('hex').substring(0, 8);
+ path.basename = `app-${hash}`;
+ global.cssFilename = `${path.basename}.css`;
+ }));
+ } else {
+ stream = stream.pipe(sourcemaps.write('.'));
+ global.cssFilename = 'app.css';
+ }
+
+ return stream.pipe(gulp.dest(paths.dist));
+}
+
+// Bundle templates and SVG assets into HTML for production
+async function bundleAssets() {
+ if (!isProduction) {
+ return Promise.resolve();
+ }
+
+ try {
+ // Read all template files
+ const templateFiles = await glob('templates/**/*.html');
+ const templates = {};
+
+ for (const templateFile of templateFiles) {
+ const content = await fs.readFile(templateFile, 'utf8');
+ const templateName = path.basename(templateFile, '.html');
+ templates[templateName] = content;
+ }
+
+ // Read SVG assets
+ const svgFiles = await glob('assets/**/*.svg');
+ const svgAssets = {};
+
+ for (const svgFile of svgFiles) {
+ const content = await fs.readFile(svgFile, 'utf8');
+ const assetName = path.relative('assets', svgFile);
+ svgAssets[assetName] = content;
+ }
+
+ // Create the bundled assets object
+ const bundledAssets = {
+ templates,
+ svg: svgAssets
+ };
+
+ // Store for use in HTML processing
+ global.bundledAssets = bundledAssets;
+ return Promise.resolve();
+ } catch (error) {
+ console.error('Error bundling assets:', error);
+ throw error;
+ }
+}
+
+// HTML processing
+async function html() {
+ const jsFile = global.jsFilename || 'app.js';
+ const cssFile = global.cssFilename || 'app.css';
+
+ let htmlContent = await fs.readFile(paths.src.html.main, 'utf8');
+
+ // Replace script and CSS references
+ htmlContent = htmlContent.replace('', ``);
+ htmlContent = htmlContent.replace('', '');
+ htmlContent = htmlContent.replace('', ``);
+
+ // In production, inject bundled assets
+ if (isProduction && global.bundledAssets) {
+ const bundledAssetsScript = `
+ `;
+
+ // Insert the bundled assets script before the main app script
+ htmlContent = htmlContent.replace(
+ ``,
+ `${bundledAssetsScript}\n`
+ );
+ }
+
+ if (isProduction) {
+ // Use htmlmin to minify the content
+ const htmlMinify = (await import('html-minifier-terser')).minify;
+ htmlContent = await htmlMinify(htmlContent, {
+ caseSensitive: false,
+ collapseBooleanAttributes: true,
+ collapseInlineTagWhitespace: false,
+ collapseWhitespace: true,
+ conservativeCollapse: false,
+ decodeEntities: true,
+ html5: true,
+ includeAutoGeneratedTags: true,
+ keepClosingSlash: false,
+ minifyCSS: true,
+ minifyJS: true,
+ minifyURLs: false,
+ preserveLineBreaks: false,
+ preventAttributesEscaping: false,
+ processConditionalComments: false,
+ removeAttributeQuotes: true,
+ removeComments: true,
+ removeEmptyAttributes: true,
+ removeEmptyElements: false,
+ removeOptionalTags: true,
+ removeRedundantAttributes: true,
+ removeScriptTypeAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ removeTagWhitespace: false,
+ sortAttributes: true,
+ sortClassName: true,
+ trimCustomFragments: true,
+ useShortDoctype: true
+ });
+ }
+
+ // Write the processed HTML file
+ await fs.writeFile(path.join(paths.dist, 'index.html'), htmlContent);
+ return Promise.resolve();
+}
+
+// Template processing (only for development builds)
+function templates() {
+ if (isProduction) {
+ // In production, templates are bundled into the HTML file
+ return Promise.resolve();
+ }
+
+ return gulp.src(paths.src.html.templates)
+ .pipe(gulp.dest(`${paths.dist}/templates`));
+}
+
+// Language files processing
+function languages() {
+ return gulp.src(paths.src.lang)
+ .pipe(gulpif(isProduction, jsonMinify()))
+ .pipe(gulp.dest(`${paths.dist}/lang`));
+}
+
+// Copy assets (SVGs are bundled in production)
+function assets() {
+ if (isProduction) {
+ // In production, SVGs are bundled into the HTML file, so only copy other assets
+ return gulp.src(paths.src.assets, { base: '.' })
+ .pipe(gulp.dest(paths.dist));
+ }
+
+ return gulp.src([...paths.src.assets, paths.src.svg], { base: '.' })
+ .pipe(gulp.dest(paths.dist));
+}
+
+// Watch task
+function watch() {
+ gulp.watch(paths.src.js.all, scripts);
+ gulp.watch(paths.src.css, styles);
+ gulp.watch([paths.src.html.main, paths.src.html.templates], gulp.series(html, templates));
+ gulp.watch(paths.src.lang, languages);
+ gulp.watch([...paths.src.assets, paths.src.svg], assets);
+}
+
+// Development task
+function dev() {
+ console.log('🚀 Development mode - watching files for changes...');
+ return watch();
+}
+
+// Build task
+const build = gulp.series(
+ clean,
+ gulp.parallel(scripts, styles),
+ bundleAssets,
+ gulp.parallel(html, templates, languages, assets)
+);
+
+// Export tasks
+export { clean, scripts, styles, bundleAssets, html, templates, languages, assets, watch, dev, build };
+export default build;
\ No newline at end of file
diff --git a/index.html b/index.html
index 237d65d..eb1cb33 100644
--- a/index.html
+++ b/index.html
@@ -314,19 +314,7 @@
gtag('js', new Date());
gtag('config', 'G-FSXPMDXLLS');
- // Wait for the module to load before calling gboot
- if (window.gboot) {
- gboot();
- } else {
- // If gboot isn't available yet, wait for it
- const checkGboot = () => {
- if (window.gboot) {
- gboot();
- } else {
- setTimeout(checkGboot, 10);
- }
- };
- checkGboot();
- }
+ // The gboot() function is now auto-called when the core.js module loads
+ // No manual initialization needed here