diff --git a/js/app.js b/js/app.js
index c5d3d36..097084c 100644
--- a/js/app.js
+++ b/js/app.js
@@ -199,31 +199,38 @@ document.addEventListener('keydown', e => {
}
});
-// ── Bootstrap: fetch config.json then start ───────────────────────────────────
+// ── Bootstrap: load config.json, then start ───────────────────────────────────
+function bootstrapWithConfig(config) {
+ setOrgData(config.orgData);
+ setDefaultAffinities(config.defaultAffinities);
+ setOwnerColors(config.ownerColors);
+ // Stamp each task with its original config name as a stable identity key.
+ // User edits can rename tasks later, so this hidden key lets storage.js
+ // reconnect saved progress to the correct starter task on reload.
+ config.orgData.departments.forEach(dept => {
+ dept.tasks.forEach(task => { task._configName = task.name; });
+ });
+ initApp();
+}
+
document.addEventListener('DOMContentLoaded', () => {
- // GitHub Pages serves this repo directly from source. Keep index.html loading
- // this file as type="module", keep imports browser-compatible, and keep
- // config.json reachable at the repo root for this fetch.
- //
- // config.json is the starter operating system for the app: departments,
- // tasks, default role affinities, and owner colors all come from there.
+ // The production build (npm run build) inlines config.json into the page as
+ // window.__PM_OPS_CONFIG__ so the downloaded dist/ folder works by double-
+ // clicking index.html — fetch() of a local file is blocked by every modern
+ // browser's file:// CORS policy, so a runtime fetch can never work there.
+ // The raw source (GitHub Pages, npm start, python -m http.server) has no
+ // such global and falls through to the original fetch, unchanged.
+ if (window.__PM_OPS_CONFIG__) {
+ bootstrapWithConfig(window.__PM_OPS_CONFIG__);
+ return;
+ }
+
fetch('config.json')
.then(r => {
if (!r.ok) throw new Error(`config.json fetch failed: ${r.status}`);
return r.json();
})
- .then(config => {
- setOrgData(config.orgData);
- setDefaultAffinities(config.defaultAffinities);
- setOwnerColors(config.ownerColors);
- // Stamp each task with its original config name as a stable identity key.
- // User edits can rename tasks later, so this hidden key lets storage.js
- // reconnect saved progress to the correct starter task on reload.
- config.orgData.departments.forEach(dept => {
- dept.tasks.forEach(task => { task._configName = task.name; });
- });
- initApp();
- })
+ .then(bootstrapWithConfig)
.catch(err => {
console.error('PM Ops Map: could not load config.json —', err);
document.getElementById('departments').innerHTML =
diff --git a/package.json b/package.json
index ef99081..253ebc0 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "pm-ops-map",
"version": "1.0.0",
"description": "A visual task ownership map for property management companies",
+ "license": "MIT",
"private": true,
"engines": {
"node": ">=20.9"
diff --git a/server/package.json b/server/package.json
index d935743..25d211d 100644
--- a/server/package.json
+++ b/server/package.json
@@ -2,6 +2,7 @@
"name": "pm-ops-map-sync-server",
"version": "1.0.0",
"description": "Optional self-hosted sync server for PM Ops Map — lets a team share one workspace across devices without a full backend.",
+ "license": "MIT",
"private": true,
"main": "index.js",
"engines": {
diff --git a/webpack.config.prod.js b/webpack.config.prod.js
index 4ae6b63..01a8961 100644
--- a/webpack.config.prod.js
+++ b/webpack.config.prod.js
@@ -1,8 +1,37 @@
+const fs = require('fs');
+const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
+// Downloading the ZIP and double-clicking index.html opens it over file://,
+// where every modern browser's CORS policy blocks fetch() of local files —
+// runtime `fetch('config.json')` can never work there, regardless of script
+// type. This plugin inlines config.json as a global for the built HTML only
+// (the raw source at the repo root is untouched and keeps using fetch(), for
+// GitHub Pages / npm start / any real HTTP server) and swaps the entry
+// script from type="module" to a plain deferred script — the webpack bundle
+// has already been compiled down to a classic IIFE with no import/export
+// left in it, so the module wrapper serves no purpose in the built output.
+class InlineConfigForFileProtocolPlugin {
+ apply(compiler) {
+ compiler.hooks.compilation.tap('InlineConfigForFileProtocolPlugin', (compilation) => {
+ HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync(
+ 'InlineConfigForFileProtocolPlugin',
+ (data, cb) => {
+ const configJson = fs.readFileSync(path.resolve(__dirname, 'config.json'), 'utf8');
+ data.html = data.html.replace(
+ '',
+ `\n`
+ );
+ cb(null, data);
+ }
+ );
+ });
+ }
+}
+
module.exports = merge(common, {
mode: 'production',
plugins: [
@@ -10,6 +39,7 @@ module.exports = merge(common, {
template: './index.html',
inject: false,
}),
+ new InlineConfigForFileProtocolPlugin(),
new CopyPlugin({
patterns: [
{ from: 'config.json', to: 'config.json' },