diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..59c3d43
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,52 @@
+name: Release Please
+on:
+ push:
+ branches: [main]
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ release-please:
+ runs-on: ubuntu-latest
+ outputs:
+ release_created: ${{ steps.release.outputs.release_created }}
+ tag_name: ${{ steps.release.outputs.tag_name }}
+ steps:
+ - uses: googleapis/release-please-action@v4
+ id: release
+ with:
+ release-type: node
+
+ docker:
+ needs: release-please
+ if: ${{ needs.release-please.outputs.release_created }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: docker/metadata-action@v5
+ id: meta
+ with:
+ images: ghcr.io/${{ github.repository }}
+ tags: |
+ type=semver,pattern={{version}},value=${{ needs.release-please.outputs.tag_name }}
+ type=semver,pattern={{major}}.{{minor}},value=${{ needs.release-please.outputs.tag_name }}
+ type=raw,value=latest
+
+ - uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..37fcefa
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "1.0.0"
+}
diff --git a/package-lock.json b/package-lock.json
index 34c3ff1..639d131 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,13 +11,15 @@
"ejs": "^4.0.1",
"express": "^4.18.2",
"fs": "^0.0.1-security",
+ "pino": "^10.3.1",
"puppeteer": "^21.0.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"sqlite3": "^5.1.7"
},
"devDependencies": {
- "nodemon": "^3.0.1"
+ "nodemon": "^3.0.1",
+ "pino-pretty": "^13.1.3"
}
},
"node_modules/@babel/code-frame": {
@@ -72,6 +74,12 @@
"node": ">=10"
}
},
+ "node_modules/@pinojs/redact": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
+ "license": "MIT"
+ },
"node_modules/@puppeteer/browsers": {
"version": "1.9.1",
"license": "Apache-2.0",
@@ -294,6 +302,15 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
+ "node_modules/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/b4a": {
"version": "1.6.7",
"license": "Apache-2.0"
@@ -620,6 +637,13 @@
"color-support": "bin.js"
}
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"license": "MIT"
@@ -697,6 +721,16 @@
"node": ">= 14"
}
},
+ "node_modules/dateformat": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+ "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/debug": {
"version": "2.6.9",
"license": "MIT",
@@ -1053,10 +1087,24 @@
"version": "2.1.3",
"license": "MIT"
},
+ "node_modules/fast-copy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz",
+ "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fast-fifo": {
"version": "1.3.2",
"license": "MIT"
},
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fd-slicer": {
"version": "1.1.0",
"license": "MIT",
@@ -1417,6 +1465,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/help-me": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
+ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -1733,6 +1788,16 @@
"node": ">=10"
}
},
+ "node_modules/joycon": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+ "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
@@ -2339,6 +2404,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"license": "MIT",
@@ -2484,6 +2558,81 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pino": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
+ "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^3.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^0.2.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^4.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
+ "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-pretty": {
+ "version": "13.1.3",
+ "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz",
+ "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.7",
+ "dateformat": "^4.6.3",
+ "fast-copy": "^4.0.0",
+ "fast-safe-stringify": "^2.1.1",
+ "help-me": "^5.0.0",
+ "joycon": "^3.1.1",
+ "minimist": "^1.2.6",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^3.0.0",
+ "pump": "^3.0.0",
+ "secure-json-parse": "^4.0.0",
+ "sonic-boom": "^4.0.1",
+ "strip-json-comments": "^5.0.2"
+ },
+ "bin": {
+ "pino-pretty": "bin.js"
+ }
+ },
+ "node_modules/pino-pretty/node_modules/strip-json-comments": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
+ "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
+ "license": "MIT"
+ },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -2544,6 +2693,22 @@
"node": ">=6"
}
},
+ "node_modules/process-warning": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
+ "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/progress": {
"version": "2.0.3",
"license": "MIT",
@@ -2950,6 +3115,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"license": "MIT",
@@ -3010,6 +3181,15 @@
"node": ">=8.10.0"
}
},
+ "node_modules/real-require": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"license": "MIT",
@@ -3068,10 +3248,36 @@
],
"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": {
"version": "2.1.2",
"license": "MIT"
},
+ "node_modules/secure-json-parse": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
+ "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/semver": {
"version": "7.7.2",
"license": "ISC",
@@ -3353,6 +3559,15 @@
"version": "2.1.3",
"license": "MIT"
},
+ "node_modules/sonic-boom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"license": "BSD-3-Clause",
@@ -3361,6 +3576,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
"node_modules/sqlite3": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
@@ -3518,6 +3742,18 @@
"b4a": "^1.6.4"
}
},
+ "node_modules/thread-stream": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
+ "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/through": {
"version": "2.3.8",
"license": "MIT"
diff --git a/package.json b/package.json
index 7c46a83..f0c1a63 100644
--- a/package.json
+++ b/package.json
@@ -11,12 +11,14 @@
"ejs": "^4.0.1",
"express": "^4.18.2",
"fs": "^0.0.1-security",
+ "pino": "^10.3.1",
"puppeteer": "^21.0.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"sqlite3": "^5.1.7"
},
"devDependencies": {
- "nodemon": "^3.0.1"
+ "nodemon": "^3.0.1",
+ "pino-pretty": "^13.1.3"
}
}
diff --git a/release-please-config.json b/release-please-config.json
new file mode 100644
index 0000000..1e0bb84
--- /dev/null
+++ b/release-please-config.json
@@ -0,0 +1,13 @@
+{
+ "packages": {
+ ".": {
+ "release-type": "node",
+ "changelog-sections": [
+ { "type": "feat", "section": "Features" },
+ { "type": "fix", "section": "Bug Fixes" },
+ { "type": "chore", "section": "Miscellaneous" },
+ { "type": "refactor", "section": "Code Refactoring" }
+ ]
+ }
+ }
+}
diff --git a/server.js b/server.js
index f5df138..7747255 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,7 @@ const { initializeDatabase, checkAndPopulateDatabase } = require('./src/db');
const playerRoutes = require('./src/routes/players');
const courseRoutes = require('./src/routes/courses');
const pageRoutes = require('./src/routes/pages');
+const logger = require('./src/logger');
const app = express();
const PORT = 3000;
@@ -22,9 +23,9 @@ initializeDatabase().then(async () => {
await checkAndPopulateDatabase();
app.listen(PORT, () => {
- console.log(`PDGA Ratings app running on http://localhost:${PORT}`);
+ logger.info(`PDGA Ratings app running on http://localhost:${PORT}`);
});
}).catch(err => {
- console.error('Failed to initialize database:', err);
+ logger.fatal('Failed to initialize database:', err);
process.exit(1);
});
diff --git a/src/db.js b/src/db.js
index 7be1f64..ed781fa 100644
--- a/src/db.js
+++ b/src/db.js
@@ -1,4 +1,5 @@
const sqlite3 = require('sqlite3').verbose();
+const logger = require('./logger');
const dbPath = process.env.DB_PATH || './ratings.db';
const db = new sqlite3.Database(dbPath);
@@ -20,13 +21,13 @@ function initializeDatabase() {
db.get("PRAGMA table_info(players)", (err, info) => {
if (err) {
- console.error('Error checking table schema:', err);
+ logger.error('Error checking table schema:', err);
return;
}
db.all("PRAGMA table_info(players)", (err, columns) => {
if (err) {
- console.error('Error getting table info:', err);
+ logger.error('Error getting table info:', err);
return;
}
@@ -35,26 +36,26 @@ function initializeDatabase() {
const hasStdDev = columns.some(col => col.name === 'std_dev');
if (!hasLastRoundUpdate) {
- console.log('Adding last_round_update column to players table...');
+ logger.info('Adding last_round_update column to players table...');
db.run(`ALTER TABLE players ADD COLUMN last_round_update DATETIME DEFAULT NULL`, (err) => {
- if (err) console.error('Error adding last_round_update column:', err.message);
- else console.log('Successfully added last_round_update column');
+ if (err) logger.error('Error adding last_round_update column:', err.message);
+ else logger.info('Successfully added last_round_update column');
});
}
if (!hasPredictedRating) {
- console.log('Adding predicted_rating column to players table...');
+ logger.info('Adding predicted_rating column to players table...');
db.run(`ALTER TABLE players ADD COLUMN predicted_rating INTEGER DEFAULT NULL`, (err) => {
- if (err) console.error('Error adding predicted_rating column:', err.message);
- else console.log('Successfully added predicted_rating column');
+ if (err) logger.error('Error adding predicted_rating column:', err.message);
+ else logger.info('Successfully added predicted_rating column');
});
}
if (!hasStdDev) {
- console.log('Adding std_dev column to players table...');
+ logger.info('Adding std_dev column to players table...');
db.run(`ALTER TABLE players ADD COLUMN std_dev INTEGER DEFAULT NULL`, (err) => {
- if (err) console.error('Error adding std_dev column:', err.message);
- else console.log('Successfully added std_dev column');
+ if (err) logger.error('Error adding std_dev column:', err.message);
+ else logger.info('Successfully added std_dev column');
});
}
});
@@ -111,7 +112,7 @@ function initializeDatabase() {
db.run(`ALTER TABLE layouts ADD COLUMN rating_count INTEGER DEFAULT 0`, () => {
db.run(`ALTER TABLE layouts ADD COLUMN last_calculated DATETIME`, () => {
db.run(`ALTER TABLE layouts ADD COLUMN last_played DATE`, () => {
- console.log('Database initialized successfully');
+ logger.info('Database initialized successfully');
resolve();
});
});
@@ -136,47 +137,47 @@ async function checkAndPopulateDatabase() {
});
if (playerCount > 0) {
- console.log(`â Database already has ${playerCount} players - skipping text file import`);
- console.log('đ Note: pdga-numbers.txt is only used when database is empty');
+ logger.info(`â Database already has ${playerCount} players - skipping text file import`);
+ logger.info('đ Note: pdga-numbers.txt is only used when database is empty');
return;
}
- console.log('=== Database is empty - populating from PDGA numbers file ===');
+ logger.info('=== Database is empty - populating from PDGA numbers file ===');
const pdgaNumbers = fs.readFileSync('pdga-numbers.txt', 'utf-8')
.split('\n')
.map(num => num.trim())
.filter(num => num);
- console.log(`Found ${pdgaNumbers.length} PDGA numbers in file`);
+ logger.info(`Found ${pdgaNumbers.length} PDGA numbers in file`);
if (pdgaNumbers.length === 0) {
- console.log('â No PDGA numbers found in file');
+ logger.info('â No PDGA numbers found in file');
return;
}
- console.log('Populating database with players from file...');
+ logger.info('Populating database with players from file...');
for (let i = 0; i < pdgaNumbers.length; i++) {
const pdgaNumber = pdgaNumbers[i];
- console.log(`[${i + 1}/${pdgaNumbers.length}] Adding PDGA ${pdgaNumber}...`);
+ logger.info(`[${i + 1}/${pdgaNumbers.length}] Adding PDGA ${pdgaNumber}...`);
try {
const playerData = await scrapePDGARating(pdgaNumber);
- console.log(` â Added ${playerData.name}`);
+ logger.info(` â Added ${playerData.name}`);
if (i < pdgaNumbers.length - 1) {
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (error) {
- console.error(` â Failed to add PDGA ${pdgaNumber}:`, error.message);
+ logger.error(` â Failed to add PDGA ${pdgaNumber}:`, error.message);
}
}
- console.log('=== Database population complete ===');
+ logger.info('=== Database population complete ===');
} catch (error) {
- console.error('Error during database population check:', error.message);
+ logger.error('Error during database population check:', error.message);
}
}
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..f429509
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,10 @@
+const pino = require('pino');
+
+const logger = pino({
+ level: process.env.LOG_LEVEL || 'info',
+ transport: process.env.NODE_ENV !== 'production'
+ ? { target: 'pino-pretty' }
+ : undefined
+});
+
+module.exports = logger;
diff --git a/src/routes/courses.js b/src/routes/courses.js
index 734f601..62bb548 100644
--- a/src/routes/courses.js
+++ b/src/routes/courses.js
@@ -4,6 +4,7 @@ const { db } = require('../db');
const { getAllCoursesFromDB, getLayoutsForCourse, updateLayoutRating } = require('../models/course');
const { launchBrowser } = require('../scrapers/browser');
const { layoutEventCache, scrapeCourseDirectory, scrapeCourseLayouts, scrapeEventResults } = require('../scrapers/course-puppeteer');
+const logger = require('../logger');
// Request locking to prevent concurrent scrapes of the same resource
const activeScrapes = new Map();
@@ -33,7 +34,7 @@ router.get('/partials/course-layouts/:courseId', async (req, res) => {
const layouts = await getLayoutsForCourse(courseId);
res.render('../partials/course-layouts', { layouts, courseId });
} catch (error) {
- console.error('Error loading course layouts:', error.message);
+ logger.error('Error loading course layouts:', error.message);
res.status(500).send('
Error loading layouts
');
}
});
@@ -43,7 +44,7 @@ router.get('/api/courses', async (req, res) => {
const courses = await getAllCoursesFromDB();
res.json(courses);
} catch (error) {
- console.error('Error fetching courses:', error.message);
+ logger.error('Error fetching courses:', error.message);
res.status(500).json({ error: 'Failed to fetch courses' });
}
});
@@ -54,7 +55,7 @@ router.get('/api/layouts/:courseId', async (req, res) => {
const layouts = await getLayoutsForCourse(courseId);
res.json(layouts);
} catch (error) {
- console.error('Error fetching layouts:', error.message);
+ logger.error('Error fetching layouts:', error.message);
res.status(500).json({ error: 'Failed to fetch layouts' });
}
});
@@ -65,7 +66,7 @@ router.post('/api/scrape-courses', async (req, res) => {
let browser = null;
try {
- console.log('Starting course directory scraping...');
+ logger.info('Starting course directory scraping...');
browser = await launchBrowser();
@@ -80,7 +81,7 @@ router.post('/api/scrape-courses', async (req, res) => {
message: `Successfully scraped ${courses.length} courses`
});
} catch (error) {
- console.error('Error scraping courses:', error.message);
+ logger.error('Error scraping courses:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}
@@ -96,7 +97,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
const lockKey = `layout-${courseId}`;
if (activeScrapes.has(lockKey)) {
- console.log(`â ī¸ Scrape already in progress for course ${courseId}`);
+ logger.info(`â ī¸ Scrape already in progress for course ${courseId}`);
return res.status(409).json({
error: 'Scrape already in progress for this course',
message: 'Please wait for the current scrape to complete'
@@ -118,19 +119,19 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
throw new Error('Course not found');
}
- console.log(`Starting layout scraping for course: ${course.name}`);
+ logger.info(`Starting layout scraping for course: ${course.name}`);
browser = await launchBrowser();
const layouts = await scrapeCourseLayouts(browser, course.link, courseId);
- console.log(`\n=== Starting event results scraping for ${course.name} ===`);
+ logger.info(`\n=== Starting event results scraping for ${course.name} ===`);
const courseIdInt = parseInt(courseId);
const layoutData = layoutEventCache.get(courseIdInt);
if (!layoutData || layoutData.length === 0) {
- console.log('No event data found in cache, skipping event results scraping');
+ logger.info('No event data found in cache, skipping event results scraping');
await browser.close();
browser = null;
@@ -183,7 +184,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
await new Promise(resolve => setTimeout(resolve, 2000));
}
- console.log(`\n=== Calculating final ratings for all layouts ===`);
+ logger.info(`\n=== Calculating final ratings for all layouts ===`);
let savedCount = 0;
for (const layoutKey in allLayoutRatings) {
@@ -194,10 +195,10 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
layoutDataResult.allRatings.reduce((sum, r) => sum + r, 0) / layoutDataResult.allRatings.length
);
- console.log(`Layout: ${layoutDataResult.name} (Par ${layoutDataResult.par})`);
- console.log(` Total ratings collected: ${layoutDataResult.allRatings.length}`);
- console.log(` Mean rating: ${meanRating}`);
- console.log(` Last played: ${layoutDataResult.latestDate || 'Unknown'}`);
+ logger.debug(`Layout: ${layoutDataResult.name} (Par ${layoutDataResult.par})`);
+ logger.debug(` Total ratings collected: ${layoutDataResult.allRatings.length}`);
+ logger.debug(` Mean rating: ${meanRating}`);
+ logger.debug(` Last played: ${layoutDataResult.latestDate || 'Unknown'}`);
try {
const changes = await updateLayoutRating(
@@ -209,11 +210,11 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
layoutDataResult.latestDate
);
if (changes > 0) {
- console.log(` â Updated in database`);
+ logger.info(` â Updated in database`);
savedCount++;
}
} catch (err) {
- console.error(` Error updating layout ${layoutDataResult.name}:`, err.message);
+ logger.error(` Error updating layout ${layoutDataResult.name}:`, err.message);
}
}
}
@@ -229,7 +230,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
message: `Successfully scraped ${layouts.length} layouts and processed ${Object.keys(eventGroups).length} events for ${course.name}`
};
} catch (error) {
- console.error('Error scraping layouts:', error.message);
+ logger.error('Error scraping layouts:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}
@@ -249,7 +250,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
});
} finally {
activeScrapes.delete(lockKey);
- console.log(`â Released lock for course ${courseId}`);
+ logger.info(`â Released lock for course ${courseId}`);
}
});
@@ -317,7 +318,7 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
await browser.close();
browser = null;
- console.log(`\n=== Calculating final ratings for all layouts ===`);
+ logger.info(`\n=== Calculating final ratings for all layouts ===`);
let savedCount = 0;
for (const layoutKey in allLayoutRatings) {
@@ -328,10 +329,10 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
ld.allRatings.reduce((sum, r) => sum + r, 0) / ld.allRatings.length
);
- console.log(`Layout: ${ld.name} (Par ${ld.par})`);
- console.log(` Total ratings collected: ${ld.allRatings.length}`);
- console.log(` Mean rating: ${meanRating}`);
- console.log(` Last played: ${ld.latestDate || 'Unknown'}`);
+ logger.debug(`Layout: ${ld.name} (Par ${ld.par})`);
+ logger.debug(` Total ratings collected: ${ld.allRatings.length}`);
+ logger.debug(` Mean rating: ${meanRating}`);
+ logger.debug(` Last played: ${ld.latestDate || 'Unknown'}`);
try {
const changes = await updateLayoutRating(
@@ -343,11 +344,11 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
ld.latestDate
);
if (changes > 0) {
- console.log(` â Updated in database`);
+ logger.info(` â Updated in database`);
savedCount++;
}
} catch (err) {
- console.error(` Error updating layout ${ld.name}:`, err.message);
+ logger.error(` Error updating layout ${ld.name}:`, err.message);
}
}
}
@@ -360,7 +361,7 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
message: `Processed ${Object.keys(eventGroups).length} events, updated ${savedCount} layouts`
});
} catch (error) {
- console.error('Error scraping event results:', error.message);
+ logger.error('Error scraping event results:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}
diff --git a/src/routes/players.js b/src/routes/players.js
index 5601c30..1a44d0d 100644
--- a/src/routes/players.js
+++ b/src/routes/players.js
@@ -7,6 +7,7 @@ const { getOfficialRatingHistory, getOptimizedPlayerRounds } = require('../scrap
const { launchBrowser } = require('../scrapers/browser');
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
const { calculatePredictedRating } = require('../services/rating-calculator');
+const logger = require('../logger');
router.get('/partials/ratings-table', async (req, res) => {
try {
@@ -28,7 +29,7 @@ router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
try {
await saveRatingHistoryToDB(pdgaNumber, history);
} catch (dbErr) {
- console.error('Failed to save rating history:', dbErr.message);
+ logger.error('Failed to save rating history:', dbErr.message);
}
}
@@ -40,7 +41,7 @@ router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
res.render('../partials/player-history', { pdgaNumber, history: formattedHistory });
} catch (error) {
- console.error('Error loading player history:', error.message);
+ logger.error('Error loading player history:', error.message);
res.status(500).send('Error loading rating history
');
}
});
@@ -92,14 +93,14 @@ router.post('/api/populate-database', (req, res) => {
res.write(`data: ${JSON.stringify(progress)}\n\n`);
};
- console.log('=== Starting database population from database players ===');
+ logger.info('=== Starting database population from database players ===');
refreshAllPlayersInDB(progressCallback).then(ratings => {
- console.log(`=== Database population complete: ${ratings.length} players refreshed ===`);
+ logger.info(`=== Database population complete: ${ratings.length} players refreshed ===`);
res.write(`data: ${JSON.stringify({ status: 'complete', ratings, message: `Successfully refreshed ${ratings.length} players` })}\n\n`);
res.end();
}).catch(error => {
- console.error('Error populating database:', error);
+ logger.error('Error populating database:', error);
res.write(`data: ${JSON.stringify({ status: 'error', message: error.message })}\n\n`);
res.end();
});
@@ -155,7 +156,7 @@ router.get('/api/rating-history/:pdgaNumber', async (req, res) => {
const cachedHistory = await getRatingHistoryFromDB(pdgaNumber);
if (cachedHistory && cachedHistory.length > 0) {
- console.log(`Using cached rating history from DB for PDGA ${pdgaNumber}`);
+ logger.info(`Using cached rating history from DB for PDGA ${pdgaNumber}`);
const formattedHistory = cachedHistory.map(row => ({
date: row.date,
rating: row.rating,
@@ -173,15 +174,15 @@ router.get('/api/rating-history/:pdgaNumber', async (req, res) => {
return;
}
- console.log(`Fetching rating history for PDGA ${pdgaNumber}...`);
+ logger.info(`Fetching rating history for PDGA ${pdgaNumber}...`);
const html = await fetchRatingHistory(pdgaNumber);
const history = parseRatingHistory(html);
try {
await saveRatingHistoryToDB(pdgaNumber, history);
- console.log(`Saved rating history for PDGA ${pdgaNumber} to database`);
+ logger.info(`Saved rating history for PDGA ${pdgaNumber} to database`);
} catch (dbErr) {
- console.error(`Failed to save rating history to database:`, dbErr.message);
+ logger.error(`Failed to save rating history to database:`, dbErr.message);
}
res.json({
@@ -189,7 +190,7 @@ router.get('/api/rating-history/:pdgaNumber', async (req, res) => {
history
});
} catch (error) {
- console.error('Error fetching rating history:', error.message);
+ logger.error('Error fetching rating history:', error.message);
res.status(500).json({ error: 'Failed to fetch rating history' });
}
});
@@ -198,19 +199,19 @@ router.post('/api/clear-cache', (req, res) => {
try {
db.run('UPDATE players SET last_updated = datetime("now", "-25 hours"), last_round_update = NULL', (err) => {
if (err) {
- console.error('Error clearing database cache:', err);
+ logger.error('Error clearing database cache:', err);
res.status(500).json({ error: 'Failed to clear database cache' });
return;
}
- console.log('Database cache cleared - all players will be refreshed on next request');
+ logger.info('Database cache cleared - all players will be refreshed on next request');
res.json({
success: true,
message: 'Cache cleared - database reset'
});
});
} catch (error) {
- console.error('Error clearing cache:', error);
+ logger.error('Error clearing cache:', error);
res.status(500).json({ error: 'Failed to clear cache' });
}
});
@@ -218,7 +219,7 @@ router.post('/api/clear-cache', (req, res) => {
router.get('/api/search-player/:pdgaNumber', async (req, res) => {
try {
const { pdgaNumber } = req.params;
- console.log(`Searching for player with PDGA number ${pdgaNumber}`);
+ logger.info(`Searching for player with PDGA number ${pdgaNumber}`);
const existingPlayer = await getPlayerFromDB(pdgaNumber);
if (existingPlayer) {
@@ -245,7 +246,7 @@ router.get('/api/search-player/:pdgaNumber', async (req, res) => {
player: playerData
});
} catch (error) {
- console.error('Error searching for player:', error.message);
+ logger.error('Error searching for player:', error.message);
res.status(500).json({ error: 'Failed to search for player' });
}
});
@@ -258,7 +259,7 @@ router.post('/api/add-player', async (req, res) => {
return res.status(400).json({ error: 'PDGA number is required' });
}
- console.log(`Adding player with PDGA number ${pdgaNumber}`);
+ logger.info(`Adding player with PDGA number ${pdgaNumber}`);
const existingPlayer = await getPlayerFromDB(pdgaNumber);
if (existingPlayer) {
@@ -281,14 +282,14 @@ router.post('/api/add-player', async (req, res) => {
await savePlayerToDB(playerData);
- console.log(`Successfully added player: ${playerData.name} (#${pdgaNumber})`);
+ logger.info(`Successfully added player: ${playerData.name} (#${pdgaNumber})`);
res.json({
success: true,
player: playerData
});
} catch (error) {
- console.error('Error adding player:', error.message);
+ logger.error('Error adding player:', error.message);
res.status(500).json({ error: 'Failed to add player' });
}
});
@@ -296,7 +297,7 @@ router.post('/api/add-player', async (req, res) => {
router.post('/api/refresh-player/:pdgaNumber', async (req, res) => {
try {
const { pdgaNumber } = req.params;
- console.log(`Manually refreshing player data for PDGA ${pdgaNumber}`);
+ logger.info(`Manually refreshing player data for PDGA ${pdgaNumber}`);
const html = await fetchPlayerDataHTTP(pdgaNumber);
const playerData = parsePlayerData(html, pdgaNumber);
@@ -308,7 +309,7 @@ router.post('/api/refresh-player/:pdgaNumber', async (req, res) => {
player: playerData
});
} catch (error) {
- console.error('Error refreshing player data:', error.message);
+ logger.error('Error refreshing player data:', error.message);
res.status(500).json({ error: 'Failed to refresh player data' });
}
});
@@ -316,31 +317,31 @@ router.post('/api/refresh-player/:pdgaNumber', async (req, res) => {
router.post('/api/refresh-rating-history/:pdgaNumber', async (req, res) => {
try {
const { pdgaNumber } = req.params;
- console.log(`=== Manually refreshing rating history for PDGA ${pdgaNumber} ===`);
+ logger.info(`=== Manually refreshing rating history for PDGA ${pdgaNumber} ===`);
const startTime = Date.now();
const html = await fetchRatingHistory(pdgaNumber);
const fetchTime = Date.now() - startTime;
- console.log(`HTML fetch completed in ${fetchTime}ms, received ${html.length} bytes`);
+ logger.info(`HTML fetch completed in ${fetchTime}ms, received ${html.length} bytes`);
const parseStartTime = Date.now();
const history = parseRatingHistory(html);
const parseTime = Date.now() - parseStartTime;
- console.log(`Parsing completed in ${parseTime}ms, found ${history.length} history entries`);
+ logger.info(`Parsing completed in ${parseTime}ms, found ${history.length} history entries`);
if (history.length > 0) {
- console.log('Sample history entries:', history.slice(0, 3));
+ logger.debug({ entries: history.slice(0, 3) }, 'Sample history entries');
} else {
- console.log('No history entries found. HTML sample:', html.substring(0, 500));
+ logger.debug({ htmlSample: html.substring(0, 500) }, 'No history entries found');
}
const dbStartTime = Date.now();
await saveRatingHistoryToDB(pdgaNumber, history);
const dbTime = Date.now() - dbStartTime;
- console.log(`Database save completed in ${dbTime}ms`);
+ logger.info(`Database save completed in ${dbTime}ms`);
const formattedHistory = history.map(entry => ({
date: entry.date,
@@ -348,16 +349,16 @@ router.post('/api/refresh-rating-history/:pdgaNumber', async (req, res) => {
displayDate: entry.displayDate
}));
- console.log(`=== Rating history refresh completed for PDGA ${pdgaNumber} ===`);
+ logger.info(`=== Rating history refresh completed for PDGA ${pdgaNumber} ===`);
res.json({
success: true,
history: formattedHistory
});
} catch (error) {
- console.error(`=== Error refreshing rating history for PDGA ${req.params.pdgaNumber} ===`);
- console.error('Error type:', error.constructor.name);
- console.error('Error message:', error.message);
+ logger.error(`=== Error refreshing rating history for PDGA ${req.params.pdgaNumber} ===`);
+ logger.error('Error type:', error.constructor.name);
+ logger.error('Error message:', error.message);
res.status(500).json({
error: 'Failed to refresh rating history',
@@ -392,7 +393,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
const isIncremental = !!sinceDate;
- console.log(`${isIncremental ? 'Incrementally updating' : 'Fully refreshing'} round history for PDGA ${pdgaNumber}${sinceDate ? ` since ${sinceDate.toDateString()}` : ''}`);
+ logger.info(`${isIncremental ? 'Incrementally updating' : 'Fully refreshing'} round history for PDGA ${pdgaNumber}${sinceDate ? ` since ${sinceDate.toDateString()}` : ''}`);
browser = await launchBrowser();
@@ -403,13 +404,13 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
await saveRatingHistoryToDB(pdgaNumber, officialHistory);
}
} catch (historyError) {
- console.error('Failed to fetch official history:', historyError.message);
+ logger.error('Failed to fetch official history:', historyError.message);
officialHistory = [];
}
let allRounds = [];
try {
- console.log(`Using optimized approach: /details + new tournaments only for PDGA ${pdgaNumber}...`);
+ logger.info(`Using optimized approach: /details + new tournaments only for PDGA ${pdgaNumber}...`);
allRounds = await getOptimizedPlayerRounds(browser, pdgaNumber);
if (allRounds.length > 0) {
@@ -420,14 +421,14 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
}));
await saveRoundHistoryToDB(pdgaNumber, roundsForDB, false);
- console.log(`â Saved ${allRounds.length} rounds using optimized approach`);
+ logger.info(`â Saved ${allRounds.length} rounds using optimized approach`);
await updateLastRoundUpdateDate(pdgaNumber);
} else {
- console.log('âš No rounds found');
+ logger.info('âš No rounds found');
}
} catch (detailsError) {
- console.error('Failed to fetch rounds using optimized approach:', detailsError.message);
+ logger.error('Failed to fetch rounds using optimized approach:', detailsError.message);
allRounds = [];
}
@@ -460,15 +461,15 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
message: `Used /details (${officialCount} rounds) + new tournaments (${newCount} rounds)`
});
} catch (error) {
- console.error(`=== Error refreshing round history for PDGA ${pdgaNumber} ===`);
- console.error('Error type:', error.constructor.name);
- console.error('Error message:', error.message);
+ logger.error(`=== Error refreshing round history for PDGA ${pdgaNumber} ===`);
+ logger.error('Error type:', error.constructor.name);
+ logger.error('Error message:', error.message);
if (browser) {
try {
await browser.close();
} catch (closeError) {
- console.error('Error closing browser:', closeError.message);
+ logger.error('Error closing browser:', closeError.message);
}
}
diff --git a/src/scrapers/course-puppeteer.js b/src/scrapers/course-puppeteer.js
index 04240f9..dcdbe60 100644
--- a/src/scrapers/course-puppeteer.js
+++ b/src/scrapers/course-puppeteer.js
@@ -1,4 +1,5 @@
const { saveCourseToDB, saveLayoutToDB } = require('../models/course');
+const logger = require('../logger');
// In-memory cache for layout-division-event mapping
const layoutEventCache = new Map();
@@ -8,7 +9,7 @@ function getLayoutEventCache() {
}
async function scrapeCourseDirectory(browser) {
- console.log('=== Scraping Swedish courses from PDGA course directory ===');
+ logger.info('Scraping Swedish courses from PDGA course directory');
const page = await browser.newPage();
const allCourses = [];
let pageNumber = 0;
@@ -18,7 +19,7 @@ async function scrapeCourseDirectory(browser) {
while (hasMorePages) {
const url = `https://www.pdga.com/course-directory/advanced?title=&field_course_location_country=SE&field_course_location_locality=&field_course_location_administrative_area=All&field_course_location_postal_code=&field_course_type_value=All&rating_value=All&field_course_holes_value=18-100&field_course_total_length_value=All&field_course_target_type_value=All&field_course_tee_type_value=All&field_location_type_value=All&field_course_camping_value=All&field_course_facilities_value=All&field_course_fees_value=All&field_course_handicap_value=All&field_course_private_value=All&field_course_signage_value=All&field_cart_friendly_value=All&page=${pageNumber}`;
- console.log(`Scraping page ${pageNumber}...`);
+ logger.info(`Scraping page ${pageNumber}...`);
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
await page.waitForTimeout(1000);
@@ -46,34 +47,34 @@ async function scrapeCourseDirectory(browser) {
});
if (courses.length === 0) {
- console.log(`No courses found on page ${pageNumber}, stopping pagination`);
+ logger.info(`No courses found on page ${pageNumber}, stopping pagination`);
hasMorePages = false;
} else {
- console.log(`Found ${courses.length} courses on page ${pageNumber}`);
+ logger.info(`Found ${courses.length} courses on page ${pageNumber}`);
allCourses.push(...courses);
for (const course of courses) {
try {
await saveCourseToDB(course);
- console.log(`â Saved course: ${course.name} (${course.city})`);
+ logger.info(`Saved course: ${course.name} (${course.city})`);
} catch (err) {
- console.error(`Error saving course ${course.name}:`, err.message);
+ logger.error(`Error saving course ${course.name}: ${err.message}`);
}
}
pageNumber++;
if (hasMorePages) {
- console.log('Waiting 2s before next page...');
+ logger.info('Waiting 2s before next page...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
- console.log(`â Total courses scraped: ${allCourses.length} across ${pageNumber} pages`);
+ logger.info(`Total courses scraped: ${allCourses.length} across ${pageNumber} pages`);
} catch (error) {
- console.error('Error scraping course directory:', error.message);
+ logger.error('Error scraping course directory: ' + error.message);
} finally {
await page.close();
}
@@ -82,7 +83,7 @@ async function scrapeCourseDirectory(browser) {
}
async function scrapeCourseLayouts(browser, courseLink, courseId) {
- console.log(`\n=== Scraping layouts from: ${courseLink} ===`);
+ logger.info(`Scraping layouts from: ${courseLink}`);
const page = await browser.newPage();
const layouts = [];
@@ -114,10 +115,10 @@ async function scrapeCourseLayouts(browser, courseLink, courseId) {
});
if (layoutsTabClicked) {
- console.log('â Layouts tab found and clicked');
+ logger.info('Layouts tab found and clicked');
await page.waitForTimeout(3000);
} else {
- console.warn('â ī¸ Layouts tab not found - may be on a single-layout course page');
+ logger.warn('Layouts tab not found - may be on a single-layout course page');
}
const extractedLayouts = await page.evaluate(() => {
@@ -198,7 +199,7 @@ async function scrapeCourseLayouts(browser, courseLink, courseId) {
const courseIdInt = typeof courseId === 'string' ? parseInt(courseId) : courseId;
layoutEventCache.set(courseIdInt, layouts);
- console.log(`â Successfully parsed ${layouts.length} layouts from course page`);
+ logger.info(`Successfully parsed ${layouts.length} layouts from course page`);
const uniqueLayouts = [];
const seen = new Set();
@@ -212,20 +213,20 @@ async function scrapeCourseLayouts(browser, courseLink, courseId) {
}
if (uniqueLayouts.length < layouts.length) {
- console.log(`âšī¸ Deduplicated to ${uniqueLayouts.length} unique layouts`);
+ logger.info(`Deduplicated to ${uniqueLayouts.length} unique layouts`);
}
for (const layout of uniqueLayouts) {
try {
await saveLayoutToDB(courseId, layout);
- console.log(` â Saved layout: ${layout.name} (Par ${layout.par})`);
+ logger.info(`Saved layout: ${layout.name} (Par ${layout.par})`);
} catch (err) {
- console.error(` â Error saving layout ${layout.name}:`, err.message);
+ logger.error(`Error saving layout ${layout.name}: ${err.message}`);
}
}
} catch (error) {
- console.error('Error scraping course layouts:', error.message);
+ logger.error('Error scraping course layouts: ' + error.message);
} finally {
await page.close();
}
@@ -332,7 +333,7 @@ async function scrapeEventResults(browser, eventUrl, layoutsWithDivisions) {
}
} catch (error) {
- console.error('Error scraping event results:', error.message);
+ logger.error('Error scraping event results: ' + error.message);
} finally {
await page.close();
}
diff --git a/src/scrapers/player-http.js b/src/scrapers/player-http.js
index 2d6f2be..46dc3ef 100644
--- a/src/scrapers/player-http.js
+++ b/src/scrapers/player-http.js
@@ -1,4 +1,5 @@
const https = require('https');
+const logger = require('../logger');
async function fetchPlayerDataHTTP(pdgaNumber) {
return new Promise((resolve, reject) => {
@@ -28,21 +29,14 @@ async function fetchPlayerDataHTTP(pdgaNumber) {
headers: res.headers
};
- console.log(`PDGA Response Status for #${pdgaNumber}: ${res.statusCode}`);
- console.log('Response Headers:', JSON.stringify(res.headers, null, 2));
+ logger.info(`PDGA Response Status for #${pdgaNumber}: ${res.statusCode}`);
- if (res.headers['retry-after']) {
- console.log(`Retry-After header: ${res.headers['retry-after']}`);
- }
- if (res.headers['x-ratelimit-limit']) {
- console.log(`Rate Limit: ${res.headers['x-ratelimit-limit']}`);
- }
- if (res.headers['x-ratelimit-remaining']) {
- console.log(`Rate Limit Remaining: ${res.headers['x-ratelimit-remaining']}`);
- }
- if (res.headers['x-ratelimit-reset']) {
- console.log(`Rate Limit Reset: ${res.headers['x-ratelimit-reset']}`);
- }
+ logger.debug({
+ retryAfter: res.headers['retry-after'],
+ rateLimit: res.headers['x-ratelimit-limit'],
+ rateLimitRemaining: res.headers['x-ratelimit-remaining'],
+ rateLimitReset: res.headers['x-ratelimit-reset']
+ }, `Rate limit details for #${pdgaNumber}`);
const error = new Error(`HTTP ${res.statusCode}`);
error.rateLimitInfo = rateLimitInfo;
@@ -52,10 +46,7 @@ async function fetchPlayerDataHTTP(pdgaNumber) {
});
req.on('error', (error) => {
- console.log(`Request error for PDGA #${pdgaNumber}:`, error.code, error.message);
- if (error.code === 'ECONNRESET') {
- console.log('Connection reset - likely rate limited by PDGA');
- }
+ logger.error(`Request error for PDGA #${pdgaNumber}: ${error.code} ${error.message}`);
reject(error);
});
@@ -88,7 +79,7 @@ function parsePlayerData(html, pdgaNumber) {
predictedRating: null
};
} catch (error) {
- console.error(`Error parsing data for PDGA ${pdgaNumber}:`, error.message);
+ logger.error(`Error parsing data for PDGA ${pdgaNumber}: ${error.message}`);
return {
pdgaNumber,
name: 'Error',
@@ -112,7 +103,7 @@ async function fetchRatingHistory(pdgaNumber) {
timeout: 30000
};
- console.log(`Fetching rating history for PDGA #${pdgaNumber} from: https://www.pdga.com/player/${pdgaNumber}/history`);
+ logger.info(`Fetching rating history for PDGA #${pdgaNumber} from: https://www.pdga.com/player/${pdgaNumber}/history`);
const req = https.request(options, (res) => {
let data = '';
@@ -122,25 +113,20 @@ async function fetchRatingHistory(pdgaNumber) {
res.on('end', () => {
if (res.statusCode === 200) {
- console.log(`Rating history request successful for PDGA #${pdgaNumber}`);
+ logger.info(`Rating history request successful for PDGA #${pdgaNumber}`);
resolve(data);
} else {
- console.log(`Rating History Error for PDGA #${pdgaNumber}:`);
- console.log(`Status: ${res.statusCode}`);
- console.log('Response Headers:', JSON.stringify(res.headers, null, 2));
+ logger.error(`Rating History Error for PDGA #${pdgaNumber}:`);
+ logger.error(`Status: ${res.statusCode}`);
- if (res.headers['retry-after']) {
- console.log(`Retry-After: ${res.headers['retry-after']} seconds`);
- }
- if (res.headers['x-ratelimit-limit']) {
- console.log(`Rate Limit: ${res.headers['x-ratelimit-limit']}`);
- }
- if (res.headers['x-ratelimit-remaining']) {
- console.log(`Rate Limit Remaining: ${res.headers['x-ratelimit-remaining']}`);
- }
+ logger.debug({
+ retryAfter: res.headers['retry-after'],
+ rateLimit: res.headers['x-ratelimit-limit'],
+ rateLimitRemaining: res.headers['x-ratelimit-remaining']
+ }, `Rate limit details for history #${pdgaNumber}`);
if (data.length > 0) {
- console.log(`Partial response received (${data.length} bytes):`, data.substring(0, 200));
+ logger.debug(`Partial response received (${data.length} bytes): ${data.substring(0, 200)}`);
}
const error = new Error(`HTTP ${res.statusCode} for rating history`);
@@ -152,28 +138,28 @@ async function fetchRatingHistory(pdgaNumber) {
});
req.on('error', (error) => {
- console.log(`Rating history request error for PDGA #${pdgaNumber}:`, {
+ logger.error({
code: error.code,
message: error.message,
errno: error.errno,
syscall: error.syscall
- });
+ }, `Rating history request error for PDGA #${pdgaNumber}`);
if (error.code === 'ECONNRESET') {
- console.log('Connection reset on rating history - likely rate limited by PDGA');
+ logger.debug('Connection reset on rating history - likely rate limited by PDGA');
}
if (error.code === 'ECONNREFUSED') {
- console.log('Connection refused - PDGA server may be blocking requests');
+ logger.debug('Connection refused - PDGA server may be blocking requests');
}
if (error.code === 'ETIMEDOUT') {
- console.log('Request timed out - server may be overloaded');
+ logger.debug('Request timed out - server may be overloaded');
}
reject(error);
});
req.on('timeout', () => {
- console.log(`Rating history request timeout for PDGA #${pdgaNumber} after 30s`);
+ logger.info(`Rating history request timeout for PDGA #${pdgaNumber} after 30s`);
req.destroy();
reject(new Error('Request timeout'));
});
diff --git a/src/scrapers/player-puppeteer.js b/src/scrapers/player-puppeteer.js
index 6ae6bec..39ce289 100644
--- a/src/scrapers/player-puppeteer.js
+++ b/src/scrapers/player-puppeteer.js
@@ -1,4 +1,5 @@
const { parseDate } = require('../services/rating-calculator');
+const logger = require('../logger');
async function getOfficialRatingHistory(browser, pdgaNumber) {
const page = await browser.newPage();
@@ -47,7 +48,7 @@ async function getOfficialRatingHistory(browser, pdgaNumber) {
});
} catch (error) {
- console.error('Error fetching official rating history:', error.message);
+ logger.error('Error fetching official rating history: ' + error.message);
} finally {
await page.close();
}
@@ -123,7 +124,7 @@ async function getPlayerTournamentDetails(browser, pdgaNumber) {
}
}
} catch (e) {
- console.log(`Date parsing failed for "${round.dateText}": ${e.message}`);
+ logger.info(`Date parsing failed for "${round.dateText}": ${e.message}`);
}
}
return {
@@ -137,7 +138,7 @@ async function getPlayerTournamentDetails(browser, pdgaNumber) {
tournamentRounds = fixedRounds;
} catch (error) {
- console.error('Error fetching tournament details:', error.message);
+ logger.error('Error fetching tournament details: ' + error.message);
} finally {
await page.close();
}
@@ -153,7 +154,7 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
const url = `https://www.pdga.com/player/${pdgaNumber}`;
await page.goto(url, { waitUntil: 'networkidle2' });
- console.log(`Looking for tournaments after ${afterDate.toDateString()}...`);
+ logger.info(`Looking for tournaments after ${afterDate.toDateString()}...`);
const newTournamentUrls = await page.evaluate((afterTimestamp) => {
const afterDate = new Date(afterTimestamp);
@@ -192,11 +193,11 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
return urls;
}, afterDate.getTime());
- console.log(`Found ${newTournamentUrls.length} new tournaments after ${afterDate.toDateString()}`);
+ logger.info(`Found ${newTournamentUrls.length} new tournaments after ${afterDate.toDateString()}`);
for (const tournamentData of newTournamentUrls) {
try {
- console.log(`Scraping new tournament: ${tournamentData.name} (${tournamentData.date})`);
+ logger.info(`Scraping new tournament: ${tournamentData.name} (${tournamentData.date})`);
await page.goto(tournamentData.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(500);
@@ -238,16 +239,16 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
});
});
- console.log(`â Found ${roundRatings.length} round ratings for ${tournamentData.name}`);
+ logger.info(`Found ${roundRatings.length} round ratings for ${tournamentData.name}`);
}
} catch (error) {
- console.error(`Error scraping tournament ${tournamentData.name}:`, error.message);
+ logger.error(`Error scraping tournament ${tournamentData.name}: ${error.message}`);
}
}
} catch (error) {
- console.error(`Error getting new tournament rounds for PDGA ${pdgaNumber}:`, error);
+ logger.error(`Error getting new tournament rounds for PDGA ${pdgaNumber}: ${error.message}`);
} finally {
await page.close();
}
@@ -256,30 +257,30 @@ async function getNewTournamentRounds(browser, pdgaNumber, afterDate) {
}
async function getOptimizedPlayerRounds(browser, pdgaNumber) {
- console.log(`=== Optimized Round Collection for PDGA ${pdgaNumber} ===`);
+ logger.info(`Optimized Round Collection for PDGA ${pdgaNumber}`);
try {
- console.log('Step 1: Getting official rating rounds from /details page...');
+ logger.info('Getting official rating rounds from /details page...');
const officialRounds = await getPlayerTournamentDetails(browser, pdgaNumber);
if (officialRounds.length === 0) {
- console.log('No official rounds found in details page');
+ logger.info('No official rounds found in details page');
return [];
}
- console.log(`â Found ${officialRounds.length} official rating rounds`);
+ logger.info(`Found ${officialRounds.length} official rating rounds`);
const sortedRounds = officialRounds.sort((a, b) => b.date - a.date);
const latestOfficialDate = sortedRounds[0].date;
- console.log(`Latest official round: ${latestOfficialDate.toDateString()}`);
+ logger.info(`Latest official round: ${latestOfficialDate.toDateString()}`);
- console.log('Step 2: Looking for NEW tournaments since latest official round...');
+ logger.info('Looking for new tournaments since latest official round...');
const newRounds = await getNewTournamentRounds(browser, pdgaNumber, latestOfficialDate);
if (newRounds.length > 0) {
- console.log(`â Found ${newRounds.length} new round ratings`);
+ logger.info(`Found ${newRounds.length} new round ratings`);
} else {
- console.log('âš No new tournaments found since latest official round');
+ logger.info('No new tournaments found since latest official round');
}
const allRounds = [
@@ -299,12 +300,12 @@ async function getOptimizedPlayerRounds(browser, pdgaNumber) {
allRounds.sort((a, b) => a.date - b.date);
- console.log(`=== Summary: ${officialRounds.length} official + ${newRounds.length} new = ${allRounds.length} total rounds ===`);
+ logger.info(`Summary: ${officialRounds.length} official + ${newRounds.length} new = ${allRounds.length} total rounds`);
return allRounds;
} catch (error) {
- console.error('Error in optimized round collection:', error.message);
+ logger.error('Error in optimized round collection: ' + error.message);
return [];
}
}
diff --git a/src/services/player-service.js b/src/services/player-service.js
index 4a62188..79f7157 100644
--- a/src/services/player-service.js
+++ b/src/services/player-service.js
@@ -2,12 +2,13 @@ const { db } = require('../db');
const { getPlayerFromDB, getRoundHistoryFromDB, savePredictedRatingToDB, savePlayerToDB } = require('../models/player');
const { fetchPlayerDataHTTP, parsePlayerData } = require('../scrapers/player-http');
const { calculatePredictedRating } = require('./rating-calculator');
+const logger = require('../logger');
async function getPlayerDataFromDB(pdgaNumber) {
try {
const cachedPlayer = await getPlayerFromDB(pdgaNumber);
if (cachedPlayer) {
- console.log(`Loading PDGA ${pdgaNumber} from DB (source of truth)`);
+ logger.debug(`Loading PDGA ${pdgaNumber} from DB (source of truth)`);
let predictedRating = cachedPlayer.predicted_rating;
let stdDev = cachedPlayer.std_dev;
@@ -28,33 +29,33 @@ async function getPlayerDataFromDB(pdgaNumber) {
}
return null;
} catch (err) {
- console.error(`Database error for PDGA ${pdgaNumber}:`, err.message);
+ logger.error(`Database error for PDGA ${pdgaNumber}:`, err.message);
return null;
}
}
async function scrapePDGARating(pdgaNumber, retries = 3) {
- console.log(`=== Refreshing PDGA ${pdgaNumber} from PDGA website ===`);
+ logger.info(`Refreshing PDGA ${pdgaNumber} from PDGA website`);
for (let attempt = 1; attempt <= retries; attempt++) {
try {
- console.log(`Attempt ${attempt}/${retries} for PDGA ${pdgaNumber} (using HTTP)`);
+ logger.info(`Attempt ${attempt}/${retries} for PDGA ${pdgaNumber} (using HTTP)`);
const html = await fetchPlayerDataHTTP(pdgaNumber);
const result = parsePlayerData(html, pdgaNumber);
try {
await savePlayerToDB(result);
- console.log(`Saved PDGA ${pdgaNumber} to database`);
+ logger.info(`Saved PDGA ${pdgaNumber} to database`);
} catch (dbErr) {
- console.error(`Failed to save PDGA ${pdgaNumber} to database:`, dbErr.message);
+ logger.error(`Failed to save PDGA ${pdgaNumber} to database:`, dbErr.message);
}
- console.log(`Successfully scraped PDGA ${pdgaNumber} on attempt ${attempt}`);
+ logger.info(`Successfully scraped PDGA ${pdgaNumber} on attempt ${attempt}`);
return result;
} catch (error) {
- console.error(`Attempt ${attempt}/${retries} failed for PDGA ${pdgaNumber}:`, error.message);
+ logger.error(`Attempt ${attempt}/${retries} failed for PDGA ${pdgaNumber}:`, error.message);
if (attempt === retries) {
return {
@@ -72,13 +73,13 @@ async function scrapePDGARating(pdgaNumber, retries = 3) {
const retryAfter = error.rateLimitInfo.headers['retry-after'];
if (retryAfter) {
retryDelay = Math.max(retryDelay, (parseInt(retryAfter) + 1) * 1000);
- console.log(`Using Retry-After header: waiting ${retryDelay/1000}s`);
+ logger.warn(`Using Retry-After header: waiting ${retryDelay/1000}s`);
}
}
if (error.code === 'ECONNRESET') {
retryDelay = Math.max(retryDelay, 10000);
- console.log(`Connection reset detected: waiting ${retryDelay/1000}s`);
+ logger.warn(`Connection reset detected: waiting ${retryDelay/1000}s`);
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
@@ -90,7 +91,7 @@ async function getPredictedRatingFromDB(pdgaNumber) {
try {
const roundHistory = await getRoundHistoryFromDB(pdgaNumber);
if (roundHistory.length > 0) {
- console.log(`Using ${roundHistory.length} cached rounds for PDGA ${pdgaNumber} prediction`);
+ logger.debug(`Using ${roundHistory.length} cached rounds for PDGA ${pdgaNumber} prediction`);
const roundRatings = roundHistory.map(round => ({
rating: round.rating,
@@ -106,7 +107,7 @@ async function getPredictedRatingFromDB(pdgaNumber) {
}
return 0;
} catch (err) {
- console.error(`Error getting predicted rating from DB for ${pdgaNumber}:`, err.message);
+ logger.error(`Error getting predicted rating from DB for ${pdgaNumber}:`, err.message);
return 0;
}
}
@@ -124,7 +125,7 @@ async function getAllRatingsFromDB(progressCallback = null) {
);
});
- console.log(`Loading ${allPlayers.length} players from database...`);
+ logger.info(`Loading ${allPlayers.length} players from database...`);
const ratings = [];
const total = allPlayers.length;
@@ -159,7 +160,7 @@ async function getAllRatingsFromDB(progressCallback = null) {
});
}
} catch (error) {
- console.error(`Failed to load PDGA ${pdgaNumber} from database:`, error.message);
+ logger.error(`Failed to load PDGA ${pdgaNumber} from database:`, error.message);
const errorData = {
pdgaNumber: parseInt(pdgaNumber),
name: player.name || 'Database Error',
@@ -183,7 +184,7 @@ async function getAllRatingsFromDB(progressCallback = null) {
return ratings.sort((a, b) => (b.rating || 0) - (a.rating || 0));
} catch (error) {
- console.error('Error loading players from database:', error);
+ logger.error('Error loading players from database:', error);
return [];
}
}
@@ -201,7 +202,7 @@ async function refreshAllPlayersInDB(progressCallback = null) {
);
});
- console.log(`Refreshing ${allPlayers.length} players from database...`);
+ logger.info(`Refreshing ${allPlayers.length} players from database...`);
const ratings = [];
const total = allPlayers.length;
@@ -210,7 +211,7 @@ async function refreshAllPlayersInDB(progressCallback = null) {
const player = allPlayers[i];
const pdgaNumber = player.pdga_number;
- console.log(`Refreshing PDGA ${pdgaNumber}... (${i + 1}/${total})`);
+ logger.info(`Refreshing PDGA ${pdgaNumber}... (${i + 1}/${total})`);
if (progressCallback) {
progressCallback({
@@ -237,7 +238,7 @@ async function refreshAllPlayersInDB(progressCallback = null) {
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (error) {
- console.error(`Failed to refresh PDGA ${pdgaNumber}:`, error.message);
+ logger.error(`Failed to refresh PDGA ${pdgaNumber}:`, error.message);
const errorData = {
pdgaNumber: parseInt(pdgaNumber),
name: player.name || 'Error',
@@ -261,7 +262,7 @@ async function refreshAllPlayersInDB(progressCallback = null) {
return ratings.sort((a, b) => (b.rating || 0) - (a.rating || 0));
} catch (error) {
- console.error('Error refreshing all players:', error);
+ logger.error('Error refreshing all players:', error);
return [];
}
}