Browse Source

feat(logging): enhance SQLite logging and IPC handler management

- Add Winston-based structured logging system with:
  - Separate console and file output formats
  - Custom SQLite and migration loggers
  - Configurable log levels and verbosity
  - Log rotation and file management
  - Type-safe logger extensions

- Improve IPC handler management:
  - Add handler registration tracking
  - Implement proper cleanup before re-registration
  - Fix handler registration conflicts
  - Add better error handling for IPC operations

- Add migration logging controls:
  - Configurable via DEBUG_MIGRATIONS env var
  - Reduced console noise while maintaining file logs
  - Structured migration status reporting

Security:
- Add proper log file permissions (0o755)
- Implement log rotation to prevent disk space issues
- Add type safety for all logging operations
- Prevent handler registration conflicts

Dependencies:
- Add winston for enhanced logging
- Remove deprecated @types/winston

This change improves debugging capabilities while reducing console noise
and fixing IPC handler registration issues that could cause database
operation failures.
sql-absurd-sql-further
Matthew Raymer 5 days ago
parent
commit
340e718199
  1. 221
      electron/package-lock.json
  2. 3
      electron/package.json
  3. 187
      electron/src/rt/logger.ts
  4. 382
      electron/src/rt/sqlite-init.ts
  5. 50
      electron/src/setup.ts

221
electron/package-lock.json

@ -22,7 +22,8 @@
"electron-updater": "^5.3.0", "electron-updater": "^5.3.0",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-fetch": "^2.6.7" "node-fetch": "^2.6.7",
"winston": "^3.17.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
@ -134,6 +135,26 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"license": "MIT",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@develar/schema-utils": { "node_modules/@develar/schema-utils": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -878,6 +899,12 @@
"integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/@types/verror": { "node_modules/@types/verror": {
"version": "1.10.11", "version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
@ -1143,7 +1170,6 @@
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-exit-hook": { "node_modules/async-exit-hook": {
@ -1690,6 +1716,16 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1708,6 +1744,31 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/colors": { "node_modules/colors": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
@ -1718,6 +1779,16 @@
"node": ">=0.1.90" "node": ">=0.1.90"
} }
}, },
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"license": "MIT",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -2464,6 +2535,12 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"license": "MIT"
},
"node_modules/encoding": { "node_modules/encoding": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
@ -2656,6 +2733,12 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT"
},
"node_modules/file-uri-to-path": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -2707,6 +2790,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT"
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@ -3269,6 +3358,12 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@ -3357,6 +3452,18 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": { "node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -3568,6 +3675,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"license": "MIT"
},
"node_modules/lazy-val": { "node_modules/lazy-val": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
@ -3651,6 +3764,23 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"license": "MIT",
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/lowercase-keys": { "node_modules/lowercase-keys": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
@ -3934,6 +4064,15 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"license": "MIT",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@ -4423,6 +4562,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -4559,6 +4707,15 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
@ -4661,6 +4818,15 @@
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/stat-mode": { "node_modules/stat-mode": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
@ -4879,6 +5045,12 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
"node_modules/through2": { "node_modules/through2": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
@ -4935,6 +5107,15 @@
"tree-kill": "cli.js" "tree-kill": "cli.js"
} }
}, },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"license": "MIT",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/truncate-utf8-bytes": { "node_modules/truncate-utf8-bytes": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@ -5101,6 +5282,42 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/winston": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
"license": "MIT",
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.7.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.9.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"license": "MIT",
"dependencies": {
"logform": "^2.7.0",
"readable-stream": "^3.6.2",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

3
electron/package.json

@ -33,7 +33,8 @@
"electron-updater": "^5.3.0", "electron-updater": "^5.3.0",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"node-fetch": "^2.6.7" "node-fetch": "^2.6.7",
"winston": "^3.17.0"
}, },
"devDependencies": { "devDependencies": {
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",

187
electron/src/rt/logger.ts

@ -1,77 +1,140 @@
/** /**
* Structured logging system for TimeSafari * Enhanced logging system for TimeSafari Electron
* Provides structured logging with proper levels and formatting
* Supports both console and file output with different verbosity levels
* *
* Provides consistent logging across the application with: * @author Matthew Raymer
* - Timestamp tracking
* - Log levels (debug, info, warn, error)
* - Structured data support
* - Component tagging
*
* @author Matthew Raymer <matthew.raymer@anomalistdesign.com>
* @version 1.0.0
* @since 2025-06-01
*/ */
// Log levels import winston from 'winston';
export enum LogLevel { import path from 'path';
DEBUG = 'DEBUG', import os from 'os';
INFO = 'INFO', import fs from 'fs';
WARN = 'WARN',
ERROR = 'ERROR' // Extend Winston Logger type with our custom loggers
declare module 'winston' {
interface Logger {
sqlite: {
debug: (message: string, ...args: unknown[]) => void;
info: (message: string, ...args: unknown[]) => void;
warn: (message: string, ...args: unknown[]) => void;
error: (message: string, ...args: unknown[]) => void;
};
migration: {
debug: (message: string, ...args: unknown[]) => void;
info: (message: string, ...args: unknown[]) => void;
warn: (message: string, ...args: unknown[]) => void;
error: (message: string, ...args: unknown[]) => void;
};
}
} }
// Log entry interface // Ensure log directory exists
interface LogEntry { const logDir = path.join(os.homedir(), 'Logs', 'TimeSafari');
timestamp: string; if (!fs.existsSync(logDir)) {
level: LogLevel; fs.mkdirSync(logDir, { recursive: true, mode: 0o755 });
component: string;
message: string;
data?: unknown;
} }
// Format log entry // Custom format for console output
const formatLogEntry = (entry: LogEntry): string => { const consoleFormat = winston.format.combine(
const { timestamp, level, component, message, data } = entry; winston.format.timestamp(),
const dataStr = data ? ` ${JSON.stringify(data, null, 2)}` : ''; winston.format.colorize(),
return `[${timestamp}] [${level}] [${component}] ${message}${dataStr}`; winston.format.printf(({ level, message, timestamp, ...metadata }) => {
}; // Skip debug logs for migrations unless explicitly enabled
if (level === 'debug' &&
typeof message === 'string' &&
message.includes('Migration') &&
!process.env.DEBUG_MIGRATIONS) {
return '';
}
// Create logger for a specific component let msg = `${timestamp} [${level}] ${message}`;
export const createLogger = (component: string) => { if (Object.keys(metadata).length > 0) {
const log = (level: LogLevel, message: string, data?: unknown) => { msg += ` ${JSON.stringify(metadata, null, 2)}`;
const entry: LogEntry = { }
timestamp: new Date().toISOString(), return msg;
level, })
component, );
message,
data
};
const formatted = formatLogEntry(entry); // Custom format for file output
const fileFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.json()
);
switch (level) { // Create logger instance
case LogLevel.DEBUG: export const logger = winston.createLogger({
console.debug(formatted); level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
break; format: fileFormat,
case LogLevel.INFO: defaultMeta: { service: 'timesafari-electron' },
console.info(formatted); transports: [
break; // Console transport with custom format
case LogLevel.WARN: new winston.transports.Console({
console.warn(formatted); format: consoleFormat,
break; level: process.env.NODE_ENV === 'development' ? 'debug' : 'info'
case LogLevel.ERROR: }),
console.error(formatted); // File transport for all logs
break; new winston.transports.File({
} filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// File transport for all logs including debug
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
}) as winston.Logger & {
sqlite: {
debug: (message: string, ...args: unknown[]) => void;
info: (message: string, ...args: unknown[]) => void;
warn: (message: string, ...args: unknown[]) => void;
error: (message: string, ...args: unknown[]) => void;
}; };
migration: {
return { debug: (message: string, ...args: unknown[]) => void;
debug: (message: string, data?: unknown) => log(LogLevel.DEBUG, message, data), info: (message: string, ...args: unknown[]) => void;
info: (message: string, data?: unknown) => log(LogLevel.INFO, message, data), warn: (message: string, ...args: unknown[]) => void;
warn: (message: string, data?: unknown) => log(LogLevel.WARN, message, data), error: (message: string, ...args: unknown[]) => void;
error: (message: string, data?: unknown) => log(LogLevel.ERROR, message, data)
}; };
}; };
// Create default logger for SQLite operations // Add SQLite specific logger
export const logger = createLogger('SQLite'); logger.sqlite = {
debug: (message: string, ...args: unknown[]) => {
logger.debug(`[SQLite] ${message}`, ...args);
},
info: (message: string, ...args: unknown[]) => {
logger.info(`[SQLite] ${message}`, ...args);
},
warn: (message: string, ...args: unknown[]) => {
logger.warn(`[SQLite] ${message}`, ...args);
},
error: (message: string, ...args: unknown[]) => {
logger.error(`[SQLite] ${message}`, ...args);
}
};
// Add migration specific logger
logger.migration = {
debug: (message: string, ...args: unknown[]) => {
if (process.env.DEBUG_MIGRATIONS) {
logger.debug(`[Migration] ${message}`, ...args);
}
},
info: (message: string, ...args: unknown[]) => {
logger.info(`[Migration] ${message}`, ...args);
},
warn: (message: string, ...args: unknown[]) => {
logger.warn(`[Migration] ${message}`, ...args);
},
error: (message: string, ...args: unknown[]) => {
logger.error(`[Migration] ${message}`, ...args);
}
};
// Export logger instance
export default logger;

382
electron/src/rt/sqlite-init.ts

@ -9,9 +9,9 @@
* - Plugin initialization and state verification * - Plugin initialization and state verification
* - Connection lifecycle management * - Connection lifecycle management
* - PRAGMA configuration for optimal performance * - PRAGMA configuration for optimal performance
* - Migration system integration * - Migration system integration with configurable logging
* - Error handling and recovery * - Error handling and recovery
* - IPC communication layer * - IPC communication layer with proper cleanup
* *
* Database Configuration: * Database Configuration:
* - Uses WAL journal mode for better concurrency * - Uses WAL journal mode for better concurrency
@ -56,6 +56,7 @@ import path from 'path';
import os from 'os'; import os from 'os';
import { runMigrations } from './sqlite-migrations'; import { runMigrations } from './sqlite-migrations';
import { logger } from './logger'; import { logger } from './logger';
import { startDatabaseOperation, endDatabaseOperation } from '../setup';
// Types for state management // Types for state management
interface PluginState { interface PluginState {
@ -597,255 +598,218 @@ export async function initializeSQLite(): Promise<void> {
} }
} }
// Add IPC handler tracking
const registeredHandlers = new Set<string>();
/** /**
* Sets up IPC handlers for SQLite operations * Removes all registered SQLite IPC handlers
* * Called before re-registering handlers to prevent duplicates
* Registers handlers for:
* - Plugin availability checks
* - Connection management
* - Query execution
* - Error retrieval
*
* Each handler includes:
* - State verification
* - Error handling
* - Detailed logging
* - Transaction safety
*
* Security:
* - Validates all incoming requests
* - Verifies plugin state
* - Maintains connection isolation
*
* @throws {Error} If handler registration fails
*/ */
export function setupSQLiteHandlers(): void { const cleanupSQLiteHandlers = (): void => {
// Remove existing handlers logger.debug('Cleaning up SQLite IPC handlers');
const handlers = [ for (const channel of registeredHandlers) {
'sqlite-is-available',
'sqlite-echo',
'sqlite-create-connection',
'sqlite-execute',
'sqlite-query',
'sqlite-run',
'sqlite-close-connection',
'sqlite-get-error'
];
handlers.forEach(handler => {
try { try {
ipcMain.removeHandler(handler); ipcMain.removeHandler(channel);
logger.debug(`Removed handler for channel: ${channel}`);
} catch (error) { } catch (error) {
logger.warn(`Error removing handler ${handler}:`, error); logger.warn(`Failed to remove handler for channel ${channel}:`, error);
} }
}); }
registeredHandlers.clear();
};
/**
* Registers an IPC handler with tracking
* @param channel The IPC channel to register
* @param handler The handler function
*/
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
if (registeredHandlers.has(channel)) {
logger.debug(`Handler already registered for channel: ${channel}, removing first`);
ipcMain.removeHandler(channel);
}
ipcMain.handle(channel, handler);
registeredHandlers.add(channel);
logger.debug(`Registered handler for channel: ${channel}`);
};
// Register handlers /**
ipcMain.handle('sqlite-is-available', async () => { * Sets up IPC handlers for SQLite operations
* Registers handlers for all SQLite operations defined in VALID_CHANNELS.invoke
* Tracks database operations to prevent reloads during critical operations
* Implements proper handler cleanup to prevent duplicates
*
* @author Matthew Raymer
*/
export function setupSQLiteHandlers(): void {
logger.info('Setting up SQLite IPC handlers');
// Clean up any existing handlers first
cleanupSQLiteHandlers();
// Handler for checking SQLite availability
registerHandler('sqlite-is-available', async () => {
logger.debug('Checking SQLite availability');
try { try {
startDatabaseOperation();
const isAvailable = await verifyPluginState(); const isAvailable = await verifyPluginState();
logger.debug('Plugin availability check:', { isAvailable }); logger.debug('SQLite availability check:', { isAvailable });
return isAvailable; return isAvailable;
} catch (error) { } catch (error) {
logger.error('Error checking plugin availability:', error); logger.error('SQLite availability check failed:', error);
return false; throw error;
} finally {
endDatabaseOperation();
} }
}); });
// Enhanced sqlite-echo handler // Handler for echo test
ipcMain.handle('sqlite-echo', async (_event, options) => { registerHandler('sqlite-echo', async (_event, { value }) => {
logger.debug('SQLite echo test:', { value });
try { try {
if (!await verifyPluginState()) { startDatabaseOperation();
throw new SQLiteError( if (!pluginState.instance) {
'Plugin not available', throw new SQLiteError('Plugin not initialized', 'sqlite-echo');
'sqlite-echo',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
}
if (!options || typeof options !== 'object' || !('value' in options)) {
throw new SQLiteError(
'Invalid echo options',
'sqlite-echo',
'SQLITE_INVALID_OPTIONS',
null,
{ options }
);
} }
const { value } = options as { value: unknown };
if (typeof value !== 'string') {
throw new SQLiteError(
'Echo value must be a string',
'sqlite-echo',
'SQLITE_INVALID_VALUE',
null,
{ value, type: typeof value }
);
}
logger.debug('Echo test:', { value, timestamp: new Date().toISOString() });
const result = await pluginState.instance.echo({ value }); const result = await pluginState.instance.echo({ value });
logger.debug('SQLite echo result:', result);
if (!result || typeof result !== 'object' || !('value' in result)) { return result;
throw new SQLiteError(
'Invalid echo result',
'sqlite-echo',
'SQLITE_INVALID_RESULT',
null,
{ result }
);
}
if (result.value !== value) {
throw new SQLiteError(
'Echo test failed - value mismatch',
'sqlite-echo',
'SQLITE_ECHO_MISMATCH',
null,
{ expected: value, received: result.value }
);
}
return result as SQLiteEchoResult;
} catch (error) { } catch (error) {
throw handleError(error, 'sqlite-echo', { options }); logger.error('SQLite echo failed:', error);
throw error;
} finally {
endDatabaseOperation();
} }
}); });
ipcMain.handle('sqlite-get-error', async () => { // Handler for creating database connection
return pluginState.lastError ? { registerHandler('sqlite-create-connection', async (_event, options: SQLiteConnectionOptions) => {
message: pluginState.lastError.message, logger.debug('Creating SQLite connection:', options);
stack: pluginState.lastError.stack,
name: pluginState.lastError.name,
context: (pluginState.lastError as SQLiteError).context
} : null;
});
// Add other handlers with proper state verification
ipcMain.handle('sqlite-create-connection', async (_event, options) => {
try { try {
if (!await verifyPluginState()) { startDatabaseOperation();
throw new SQLiteError('Plugin not available', 'sqlite-create-connection'); if (!pluginState.instance) {
throw new SQLiteError('Plugin not initialized', 'sqlite-create-connection');
} }
const validatedOptions = validateConnectionOptions(options);
// ... rest of connection creation logic ... await pluginState.instance.createConnection(validatedOptions);
logger.debug('SQLite connection created successfully');
return true;
} catch (error) { } catch (error) {
throw handleError(error, 'sqlite-create-connection'); logger.error('SQLite connection creation failed:', error);
throw error;
} finally {
endDatabaseOperation();
} }
}); });
// Enhanced sqlite-run handler // Handler for executing SQL statements
ipcMain.handle('sqlite-run', async (_event, options) => { registerHandler('sqlite-execute', async (_event, options: SQLiteExecuteOptions) => {
logger.debug('Executing SQL statements:', options);
try { try {
if (!await verifyPluginState()) { startDatabaseOperation();
throw new SQLiteError( if (!pluginState.instance) {
'Plugin not available', throw new SQLiteError('Plugin not initialized', 'sqlite-execute');
'sqlite-run',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
} }
const result = await pluginState.instance.execute(options);
const runOptions = validateQueryOptions(options); logger.debug('SQL execution result:', result);
const runWithDb = { return result;
database: 'timesafari',
...runOptions
};
logger.debug('Running SQL statement:', {
...runWithDb,
timestamp: new Date().toISOString()
});
const result = await pluginState.instance.run(runWithDb);
if (!result) {
throw new SQLiteError(
'Run operation returned no result',
'sqlite-run',
'SQLITE_NO_RESULT',
null,
{ run: runWithDb }
);
}
// Validate result structure
if (!('changes' in result)) {
throw new SQLiteError(
'Invalid run result structure',
'sqlite-run',
'SQLITE_INVALID_RESULT',
null,
{ result }
);
}
return result as SQLiteResult;
} catch (error) { } catch (error) {
throw handleError(error, 'sqlite-run', { options }); logger.error('SQL execution failed:', error);
throw error;
} finally {
endDatabaseOperation();
} }
}); });
// Enhanced sqlite-query handler // Handler for querying data
ipcMain.handle('sqlite-query', async (_event, options) => { registerHandler('sqlite-query', async (_event, options: SQLiteQueryOptions) => {
logger.debug('Querying SQLite:', options);
try { try {
if (!await verifyPluginState()) { startDatabaseOperation();
throw new SQLiteError( if (!pluginState.instance) {
'Plugin not available', throw new SQLiteError('Plugin not initialized', 'sqlite-query');
'sqlite-query',
'SQLITE_PLUGIN_UNAVAILABLE',
null,
{ pluginState }
);
} }
const result = await pluginState.instance.query(options);
logger.debug('SQL query result:', result);
return result;
} catch (error) {
logger.error('SQL query failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
const queryOptions = validateQueryOptions(options); // Handler for running SQL statements
const queryWithDb = { registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
database: 'timesafari', logger.debug('Running SQL statement:', options);
...queryOptions try {
}; startDatabaseOperation();
if (!pluginState.instance) {
logger.debug('Executing SQL query:', { throw new SQLiteError('Plugin not initialized', 'sqlite-run');
...queryWithDb,
timestamp: new Date().toISOString()
});
const result = await pluginState.instance.query(queryWithDb);
if (!result) {
throw new SQLiteError(
'Query operation returned no result',
'sqlite-query',
'SQLITE_NO_RESULT',
null,
{ query: queryWithDb }
);
} }
const result = await pluginState.instance.run(options);
logger.debug('SQL run result:', result);
return result;
} catch (error) {
logger.error('SQL run failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
// Validate result structure // Handler for closing database connection
if (!('values' in result) && !('changes' in result)) { registerHandler('sqlite-close-connection', async (_event, options: { database: string }) => {
throw new SQLiteError( logger.debug('Closing SQLite connection:', options);
'Invalid query result structure', try {
'sqlite-query', startDatabaseOperation();
'SQLITE_INVALID_RESULT', if (!pluginState.instance) {
null, throw new SQLiteError('Plugin not initialized', 'sqlite-close-connection');
{ result }
);
} }
await pluginState.instance.closeConnection(options);
logger.debug('SQLite connection closed successfully');
return true;
} catch (error) {
logger.error('SQLite connection close failed:', error);
throw error;
} finally {
endDatabaseOperation();
}
});
return result as SQLiteResult; // Handler for getting database path
registerHandler('get-path', async () => {
logger.debug('Getting database path');
try {
startDatabaseOperation();
const dbDir = await initializeDatabasePaths();
const dbPath = path.join(dbDir, 'timesafariSQLite.db');
logger.debug('Database path:', dbPath);
return dbPath;
} catch (error) { } catch (error) {
throw handleError(error, 'sqlite-query', { options }); logger.error('Failed to get database path:', error);
throw error;
} finally {
endDatabaseOperation();
} }
}); });
logger.info('SQLite IPC handlers registered successfully', { // Handler for getting base path
handlers: handlers.join(', '), registerHandler('get-base-path', async () => {
timestamp: new Date().toISOString() logger.debug('Getting base path');
try {
startDatabaseOperation();
const dbDir = await initializeDatabasePaths();
logger.debug('Base path:', dbDir);
return dbDir;
} catch (error) {
logger.error('Failed to get base path:', error);
throw error;
} finally {
endDatabaseOperation();
}
}); });
logger.info('SQLite IPC handlers setup complete');
} }

50
electron/src/setup.ts

@ -15,6 +15,7 @@ import { join } from 'path';
/** /**
* Reload watcher configuration and state management * Reload watcher configuration and state management
* Prevents infinite reload loops and implements rate limiting * Prevents infinite reload loops and implements rate limiting
* Also prevents reloads during critical database operations
* *
* @author Matthew Raymer * @author Matthew Raymer
*/ */
@ -22,7 +23,38 @@ const RELOAD_CONFIG = {
DEBOUNCE_MS: 1500, DEBOUNCE_MS: 1500,
COOLDOWN_MS: 5000, COOLDOWN_MS: 5000,
MAX_RELOADS_PER_MINUTE: 10, MAX_RELOADS_PER_MINUTE: 10,
MAX_RELOADS_PER_SESSION: 100 MAX_RELOADS_PER_SESSION: 100,
DATABASE_OPERATION_TIMEOUT_MS: 10000 // 10 second timeout for database operations
};
// Track database operation state
let isDatabaseOperationInProgress = false;
let lastDatabaseOperationTime = 0;
/**
* Checks if a database operation is in progress or recently completed
* @returns {boolean} Whether a database operation is active
*/
const isDatabaseOperationActive = (): boolean => {
const now = Date.now();
return isDatabaseOperationInProgress ||
(now - lastDatabaseOperationTime < RELOAD_CONFIG.DATABASE_OPERATION_TIMEOUT_MS);
};
/**
* Marks the start of a database operation
*/
export const startDatabaseOperation = (): void => {
isDatabaseOperationInProgress = true;
lastDatabaseOperationTime = Date.now();
};
/**
* Marks the end of a database operation
*/
export const endDatabaseOperation = (): void => {
isDatabaseOperationInProgress = false;
lastDatabaseOperationTime = Date.now();
}; };
const reloadWatcher = { const reloadWatcher = {
@ -45,12 +77,18 @@ const resetReloadCounter = () => {
}; };
/** /**
* Checks if a reload is allowed based on rate limits and cooldown * Checks if a reload is allowed based on rate limits, cooldown, and database state
* @returns {boolean} Whether a reload is allowed * @returns {boolean} Whether a reload is allowed
*/ */
const canReload = (): boolean => { const canReload = (): boolean => {
const now = Date.now(); const now = Date.now();
// Check if database operation is active
if (isDatabaseOperationActive()) {
console.warn('[Reload Watcher] Skipping reload - database operation in progress');
return false;
}
// Check cooldown period // Check cooldown period
if (now - reloadWatcher.lastReloadTime < RELOAD_CONFIG.COOLDOWN_MS) { if (now - reloadWatcher.lastReloadTime < RELOAD_CONFIG.COOLDOWN_MS) {
console.warn('[Reload Watcher] Skipping reload - cooldown period active'); console.warn('[Reload Watcher] Skipping reload - cooldown period active');
@ -342,6 +380,14 @@ export class ElectronCapacitorApp {
if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) { if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) {
this.MainWindow.show(); this.MainWindow.show();
} }
// Re-register SQLite handlers after reload
if (electronIsDev) {
console.log('[Electron Main Process] Re-registering SQLite handlers after reload');
const { setupSQLiteHandlers } = require('./rt/sqlite-init');
setupSQLiteHandlers();
}
setTimeout(() => { setTimeout(() => {
if (electronIsDev) { if (electronIsDev) {
this.MainWindow.webContents.openDevTools(); this.MainWindow.webContents.openDevTools();

Loading…
Cancel
Save