mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
chore(version): single-source VERSION, env-configurable data paths, bump tooling
- server/version.js: shared version helper that reads the root VERSION file once (fallback 0.0.0). Replaces the stale hardcoded 1.2.0 / 1.5.1 / 1.0.0 fallbacks in /api/version, /api/update/check, and /api/status. - config.js: DATA_DIR / DB_PATH / UPLOADS_DIR / CERTS_DIR env overrides for the db, uploads, and certs/jwt-secret locations. Unset resolves to exactly the legacy in-repo paths, so existing installs (including production) are byte-for-byte unchanged. Guarded by test/config-paths.test.js. - package.json: rename remote-display-server -> screentinker (+ lockfile name). - scripts/bump-version.sh: one-shot bump across VERSION, package.json (+lock), android (versionName and versionCode + 1), and the tizen widget version; makes one commit plus an annotated tag; prints the push command, never pushes. - .gitignore: global *.db / *.db-wal / *.db-shm / *.db.* so no database file (including .db.devbak backups, at any path) can be committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
26cd29c530
commit
52b10408be
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,10 +1,12 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Database
|
||||
server/db/*.db
|
||||
server/db/*.db-wal
|
||||
server/db/*.db-shm
|
||||
# Databases: SQLite files, WAL/SHM sidecars, and any .db.<suffix> backups
|
||||
# (e.g. .db.devbak), anywhere in the tree - never commit a database.
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
*.db.*
|
||||
|
||||
# Uploads (user content)
|
||||
server/uploads/
|
||||
|
|
|
|||
57
scripts/bump-version.sh
Executable file
57
scripts/bump-version.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
#!/bin/bash
|
||||
# Bump the ScreenTinker version across every source of truth in one commit + tag.
|
||||
#
|
||||
# scripts/bump-version.sh major|minor|patch|X.Y.Z
|
||||
#
|
||||
# Updates (and commits together): VERSION (root, the value the server reads at
|
||||
# runtime), server/package.json + package-lock.json, android versionName
|
||||
# (+versionCode by 1), tizen/config.xml widget version. Then creates an annotated
|
||||
# tag vX.Y.Z. Does NOT push - prints the push command, so a release fires
|
||||
# deliberately (pushing the tag is what triggers the release workflow).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Require a clean tree so the version commit can't sweep up unrelated changes.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "ERROR: working tree is dirty - commit or stash before bumping." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT="$(cat VERSION)"
|
||||
IFS=. read -r MAJ MIN PAT <<< "$CURRENT"
|
||||
|
||||
case "${1:-}" in
|
||||
major) NEW="$((MAJ + 1)).0.0" ;;
|
||||
minor) NEW="${MAJ}.$((MIN + 1)).0" ;;
|
||||
patch) NEW="${MAJ}.${MIN}.$((PAT + 1))" ;;
|
||||
[0-9]*.[0-9]*.[0-9]*) NEW="$1" ;;
|
||||
*) echo "usage: $0 major|minor|patch|X.Y.Z (current: $CURRENT)" >&2; exit 1 ;;
|
||||
esac
|
||||
echo "Bumping $CURRENT -> $NEW"
|
||||
|
||||
# 1) VERSION (source of truth)
|
||||
printf '%s\n' "$NEW" > VERSION
|
||||
|
||||
# 2) server/package.json version + lockfile (only the top-level "version" key;
|
||||
# dependency entries are "name": "^x.y.z" and won't match "version": "x.y.z")
|
||||
sed -i -E "s/(\"version\"[[:space:]]*:[[:space:]]*)\"[0-9]+\.[0-9]+\.[0-9]+\"/\1\"$NEW\"/" server/package.json
|
||||
( cd server && npm install --package-lock-only >/dev/null )
|
||||
|
||||
# 3) android versionName + versionCode (+1)
|
||||
sed -i -E "s/(versionName[[:space:]]*=[[:space:]]*)\"[0-9.]+\"/\1\"$NEW\"/" android/app/build.gradle.kts
|
||||
CODE="$(grep -oE 'versionCode[[:space:]]*=[[:space:]]*[0-9]+' android/app/build.gradle.kts | grep -oE '[0-9]+$')"
|
||||
sed -i -E "s/(versionCode[[:space:]]*=[[:space:]]*)[0-9]+/\1$((CODE + 1))/" android/app/build.gradle.kts
|
||||
|
||||
# 4) tizen widget version. Leading-space guard targets the widget's version="..."
|
||||
# attribute and NOT tizen:application required_version="..." (no space before
|
||||
# "version" there - it's "...d_version").
|
||||
sed -i -E "s/([[:space:]]version=\")[0-9][^\"]*(\")/\1${NEW}\2/" tizen/config.xml
|
||||
|
||||
# 5) commit + annotated tag (no push)
|
||||
git add VERSION server/package.json server/package-lock.json android/app/build.gradle.kts tizen/config.xml
|
||||
git commit -q -m "chore(release): v$NEW"
|
||||
git tag -a "v$NEW" -m "ScreenTinker v$NEW"
|
||||
|
||||
echo
|
||||
echo "Committed + tagged v$NEW (nothing pushed). To release:"
|
||||
echo " git push origin main && git push origin v$NEW"
|
||||
|
|
@ -1,12 +1,23 @@
|
|||
const path = require('path');
|
||||
|
||||
// Data locations. Everything defaults to the in-repo layout, so existing installs
|
||||
// (including production) are byte-for-byte unchanged when these are unset. Set
|
||||
// DATA_DIR - or the individual *_PATH / *_DIR vars - to relocate state onto a
|
||||
// mounted volume (used by the Docker image). UNSET resolves to exactly the legacy
|
||||
// paths: server/db/remote_display.db, server/uploads/, server/certs/.
|
||||
const DATA_DIR = process.env.DATA_DIR || __dirname;
|
||||
const uploadsDir = process.env.UPLOADS_DIR || path.join(DATA_DIR, 'uploads');
|
||||
const certsDir = process.env.CERTS_DIR || path.join(DATA_DIR, 'certs');
|
||||
|
||||
module.exports = {
|
||||
port: process.env.PORT || 3001,
|
||||
httpsPort: process.env.HTTPS_PORT || 3443,
|
||||
dbPath: path.join(__dirname, 'db', 'remote_display.db'),
|
||||
uploadsDir: path.join(__dirname, 'uploads'),
|
||||
contentDir: path.join(__dirname, 'uploads', 'content'),
|
||||
screenshotsDir: path.join(__dirname, 'uploads', 'screenshots'),
|
||||
dataDir: DATA_DIR,
|
||||
dbPath: process.env.DB_PATH || path.join(DATA_DIR, 'db', 'remote_display.db'),
|
||||
uploadsDir,
|
||||
contentDir: path.join(uploadsDir, 'content'),
|
||||
screenshotsDir: path.join(uploadsDir, 'screenshots'),
|
||||
certsDir,
|
||||
frontendDir: path.join(__dirname, '..', 'frontend'),
|
||||
// App-level heartbeat. Checker runs every heartbeatInterval and marks
|
||||
// devices offline if last_heartbeat is older than heartbeatTimeout.
|
||||
|
|
@ -29,11 +40,11 @@ module.exports = {
|
|||
screenshotQuality: 70,
|
||||
// SSL: drop your Cloudflare Origin cert + key in certs/ folder
|
||||
// or set env vars SSL_CERT and SSL_KEY to custom paths
|
||||
sslCert: process.env.SSL_CERT || path.join(__dirname, 'certs', 'cert.pem'),
|
||||
sslKey: process.env.SSL_KEY || path.join(__dirname, 'certs', 'key.pem'),
|
||||
sslCert: process.env.SSL_CERT || path.join(certsDir, 'cert.pem'),
|
||||
sslKey: process.env.SSL_KEY || path.join(certsDir, 'key.pem'),
|
||||
// Auth
|
||||
jwtSecret: process.env.JWT_SECRET || (() => {
|
||||
const secretFile = path.join(__dirname, 'certs', '.jwt_secret');
|
||||
const secretFile = path.join(certsDir, '.jwt_secret');
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(secretFile)) return fs.readFileSync(secretFile, 'utf8').trim();
|
||||
const secret = require('crypto').randomBytes(64).toString('hex');
|
||||
|
|
|
|||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "remote-display-server",
|
||||
"name": "screentinker",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "remote-display-server",
|
||||
"name": "screentinker",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^5.2.1",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "remote-display-server",
|
||||
"name": "screentinker",
|
||||
"version": "1.0.0",
|
||||
"description": "ScreenTinker - Digital Signage Management Server",
|
||||
"main": "server.js",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const os = require('os');
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const config = require('../config');
|
||||
const VERSION = require('../version');
|
||||
const { PLATFORM_ROLES } = require('../middleware/auth');
|
||||
|
||||
// Public status page
|
||||
|
|
@ -16,8 +17,7 @@ router.get('/', (req, res) => {
|
|||
const uptime = process.uptime();
|
||||
|
||||
// Public status - minimal info only (no user counts, no server internals)
|
||||
let version = '1.5.1';
|
||||
try { version = require('fs').readFileSync(require('path').join(__dirname, '..', '..', 'VERSION'), 'utf8').trim(); } catch {}
|
||||
const version = VERSION;
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const cors = require('cors');
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const config = require('./config');
|
||||
const VERSION = require('./version');
|
||||
|
||||
// Ensure upload directories exist
|
||||
[config.contentDir, config.screenshotsDir].forEach(dir => {
|
||||
|
|
@ -469,9 +470,7 @@ updateFrontendHash();
|
|||
// Recheck every 30 seconds
|
||||
setInterval(updateFrontendHash, 30000);
|
||||
app.get('/api/version', (req, res) => {
|
||||
let version = '1.2.0';
|
||||
try { version = fs.readFileSync(path.join(__dirname, '..', 'VERSION'), 'utf8').trim(); } catch {}
|
||||
res.json({ hash: frontendHash, version });
|
||||
res.json({ hash: frontendHash, version: VERSION });
|
||||
});
|
||||
|
||||
// Public status page
|
||||
|
|
@ -488,13 +487,7 @@ app.get('/api/update/check', (req, res) => {
|
|||
const apkSize = apkExists ? fs.statSync(apkPath).size : 0;
|
||||
const apkModified = apkExists ? fs.statSync(apkPath).mtimeMs : 0;
|
||||
|
||||
// Read version from a version file, or use the APK modification time as a version indicator
|
||||
const versionFile = path.join(__dirname, '..', 'VERSION');
|
||||
let latestVersion = '1.0.0';
|
||||
try {
|
||||
if (fs.existsSync(versionFile)) latestVersion = fs.readFileSync(versionFile, 'utf8').trim();
|
||||
} catch {}
|
||||
|
||||
const latestVersion = VERSION;
|
||||
const updateAvailable = currentVersion && currentVersion !== latestVersion;
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
44
server/test/config-paths.test.js
Normal file
44
server/test/config-paths.test.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Hard backward-compat guarantee: with DATA_DIR (and the per-path overrides)
|
||||
// UNSET, config must resolve to exactly the legacy in-repo locations, so existing
|
||||
// installs - including production - see zero behavior change. Also verifies the
|
||||
// overrides actually relocate state (the Docker /data case).
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const path = require('node:path');
|
||||
|
||||
const PATH_ENV = ['DATA_DIR', 'DB_PATH', 'UPLOADS_DIR', 'CERTS_DIR'];
|
||||
const serverDir = path.join(__dirname, '..'); // config.js lives in server/
|
||||
|
||||
function loadConfig(overrides) {
|
||||
PATH_ENV.forEach((k) => delete process.env[k]);
|
||||
process.env.JWT_SECRET = 'test-secret'; // short-circuits the secret-file-writing IIFE (no FS side effects)
|
||||
Object.assign(process.env, overrides || {});
|
||||
delete require.cache[require.resolve('../config')];
|
||||
return require('../config');
|
||||
}
|
||||
|
||||
test('UNSET -> exactly the legacy in-repo paths (zero change for existing installs)', () => {
|
||||
const c = loadConfig();
|
||||
assert.strictEqual(c.dataDir, serverDir);
|
||||
assert.strictEqual(c.dbPath, path.join(serverDir, 'db', 'remote_display.db'));
|
||||
assert.strictEqual(c.uploadsDir, path.join(serverDir, 'uploads'));
|
||||
assert.strictEqual(c.contentDir, path.join(serverDir, 'uploads', 'content'));
|
||||
assert.strictEqual(c.screenshotsDir, path.join(serverDir, 'uploads', 'screenshots'));
|
||||
assert.strictEqual(c.certsDir, path.join(serverDir, 'certs'));
|
||||
});
|
||||
|
||||
test('DATA_DIR relocates db / uploads / certs onto the volume', () => {
|
||||
const c = loadConfig({ DATA_DIR: '/data' });
|
||||
assert.strictEqual(c.dbPath, path.join('/data', 'db', 'remote_display.db'));
|
||||
assert.strictEqual(c.uploadsDir, path.join('/data', 'uploads'));
|
||||
assert.strictEqual(c.contentDir, path.join('/data', 'uploads', 'content'));
|
||||
assert.strictEqual(c.screenshotsDir, path.join('/data', 'uploads', 'screenshots'));
|
||||
assert.strictEqual(c.certsDir, path.join('/data', 'certs'));
|
||||
});
|
||||
|
||||
test('individual overrides win over DATA_DIR', () => {
|
||||
const c = loadConfig({ DATA_DIR: '/data', DB_PATH: '/custom/app.db', UPLOADS_DIR: '/media' });
|
||||
assert.strictEqual(c.dbPath, '/custom/app.db');
|
||||
assert.strictEqual(c.uploadsDir, '/media');
|
||||
assert.strictEqual(c.contentDir, path.join('/media', 'content'));
|
||||
});
|
||||
13
server/version.js
Normal file
13
server/version.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Single source of truth for the running version string. Reads the root VERSION
|
||||
// file once at load (the version only changes across a deploy, which restarts the
|
||||
// process). Fallback '0.0.0' so a stale literal can never masquerade as a real
|
||||
// release - replaces the old hardcoded '1.2.0' / '1.5.1' fallbacks.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let version = '0.0.0';
|
||||
try {
|
||||
version = fs.readFileSync(path.join(__dirname, '..', 'VERSION'), 'utf8').trim() || '0.0.0';
|
||||
} catch { /* keep the 0.0.0 fallback */ }
|
||||
|
||||
module.exports = version;
|
||||
Loading…
Reference in a new issue