From 0ce9817e32395480204d60ea8229796cd693185d Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira Date: Tue, 27 Jan 2026 05:19:48 +0000 Subject: [PATCH 01/17] Update release.yml --- .github/workflows/release.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fbc643..b90d8d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,19 @@ on: push: tags: - 'v*.*.*' + workflow_dispatch: + inputs: + publish: + description: 'Choose where you want to publish the build' + required: true + default: AppTester + type: choice + options: + - GooglePlay + - AppTester + - Both + + jobs: test: From 88af0ef5cb6be1124d58bdf900534eb77ff37caf Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira Date: Tue, 27 Jan 2026 05:30:20 +0000 Subject: [PATCH 02/17] Update release.yml --- .github/workflows/release.yml | 219 +++++++++++++++++----------------- 1 file changed, 107 insertions(+), 112 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b90d8d8..bf62288 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,134 +5,129 @@ on: tags: - 'v*.*.*' workflow_dispatch: - inputs: - publish: - description: 'Choose where you want to publish the build' - required: true - default: AppTester - type: choice - options: - - GooglePlay - - AppTester - - Both - +permissions: + contents: write + packages: write jobs: test: name: Test runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test:ci - env: - SUPPRESS_LOGS: 'true' - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results - path: ./test-results/junit.xml - - - name: Upload coverage reports - uses: codecov/codecov-action@v4 - with: - directory: ./coverage/ - token: ${{ secrets.CODECOV_TOKEN }} + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test:ci + env: + SUPPRESS_LOGS: 'true' + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: ./test-results/junit.xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + with: + directory: ./coverage/ + token: ${{ secrets.CODECOV_TOKEN }} release: name: Create Release runs-on: ubuntu-latest needs: test - + steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version from tag - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Extract release notes from CHANGELOG.md - id: extract_release_notes - run: | - VERSION=$(echo ${{ steps.get_version.outputs.VERSION }} | sed 's/^v//') - # Extract section for current version from CHANGELOG - SECTION=$(sed -n "/## \\[v${VERSION}\\]/,/## \\[v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\]/p" CHANGELOG.md | sed '$d') - - # If no section found, use a default message - if [ -z "$SECTION" ]; then - SECTION="## [v${VERSION}] - $(date +%Y-%m-%d)\nNo detailed release notes available." - fi - - # Escape newlines and special characters for GitHub Actions - RELEASE_NOTES="${SECTION//'%'/'%25'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" - - echo "RELEASE_NOTES<> $GITHUB_OUTPUT - echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.get_version.outputs.VERSION }} - name: Release ${{ steps.get_version.outputs.VERSION }} - body: ${{ steps.extract_release_notes.outputs.RELEASE_NOTES }} - draft: false - prerelease: false - token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG.md + id: extract_release_notes + run: | + VERSION=$(echo ${{ steps.get_version.outputs.VERSION }} | sed 's/^v//') + SECTION=$(sed -n "/## \\[v${VERSION}\\]/,/## \\[v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\]/p" CHANGELOG.md | sed '$d') + + if [ -z "$SECTION" ]; then + SECTION="## [v${VERSION}] - $(date +%Y-%m-%d)\nNo detailed release notes available." + fi + + RELEASE_NOTES="${SECTION//'%'/'%25'}" + RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" + RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" + + echo "RELEASE_NOTES<> $GITHUB_OUTPUT + echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.get_version.outputs.VERSION }} + name: Release ${{ steps.get_version.outputs.VERSION }} + body: ${{ steps.extract_release_notes.outputs.RELEASE_NOTES }} + draft: false + prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} docker: name: Build and Push Docker Image runs-on: ubuntu-latest needs: release - + steps: - - uses: actions/checkout@v4 - - - name: Get version from tag - id: get_version - run: | - VERSION=${GITHUB_REF#refs/tags/} - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }} - ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION_NO_V }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.get_version.outputs.VERSION_NO_V }} + - uses: actions/checkout@v4 + + - name: Get version from tag + id: get_version + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: Set image name (lowercase) + id: image + run: | + IMAGE="ghcr.io/${GITHUB_REPOSITORY}" + echo "IMAGE_LC=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ${{ steps.image.outputs.IMAGE_LC }}:latest + ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION }} + ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION_NO_V }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ steps.get_version.outputs.VERSION_NO_V }} From 011dc632fd685dbcf48473c24c5ad7ef8bd403ad Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira Date: Tue, 27 Jan 2026 05:39:34 +0000 Subject: [PATCH 03/17] Update release.yml --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf62288..593d91f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,11 +39,11 @@ jobs: name: test-results path: ./test-results/junit.xml - - name: Upload coverage reports - uses: codecov/codecov-action@v4 - with: - directory: ./coverage/ - token: ${{ secrets.CODECOV_TOKEN }} +# - name: Upload coverage reports +# uses: codecov/codecov-action@v4 +# with: +# directory: ./coverage/ +# token: ${{ secrets.CODECOV_TOKEN }} release: name: Create Release From 6e7336bd79fa5e6da91b408e0069c6aff7e66473 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira Date: Tue, 27 Jan 2026 14:55:13 +0000 Subject: [PATCH 04/17] Update release.yml --- .github/workflows/release.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 593d91f..13a86d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,26 +18,26 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' +# - name: Use Node.js +# uses: actions/setup-node@v4 +# with: +# node-version: '20' +# cache: 'npm' - - name: Install dependencies - run: npm ci +# - name: Install dependencies +# run: npm ci - - name: Run tests - run: npm run test:ci - env: - SUPPRESS_LOGS: 'true' +# - name: Run tests +# run: npm run test:ci +# env: +# SUPPRESS_LOGS: 'true' - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results - path: ./test-results/junit.xml +# - name: Upload test results +# uses: actions/upload-artifact@v4 +# if: always() +# with: +# name: test-results +# path: ./test-results/junit.xml # - name: Upload coverage reports # uses: codecov/codecov-action@v4 From bd4d08a79a56c3bfa4546b5812d4ec1cc3191847 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:07:37 +0100 Subject: [PATCH 05/17] Update release.yml --- .github/workflows/release.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13a86d8..f75dc35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,11 @@ on: tags: - 'v*.*.*' workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., v1.0.0)' + required: true + type: string permissions: contents: write @@ -57,7 +62,12 @@ jobs: - name: Get version from tag id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "VERSION=${{ inputs.version }}" >> $GITHUB_OUTPUT + else + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi - name: Extract release notes from CHANGELOG.md id: extract_release_notes @@ -98,7 +108,11 @@ jobs: - name: Get version from tag id: get_version run: | - VERSION=${GITHUB_REF#refs/tags/} + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + else + VERSION=${GITHUB_REF#refs/tags/} + fi echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT From ac0e967325a3da67a5353e91a0bea94bdddfb642 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:18:40 +0100 Subject: [PATCH 06/17] add dockerignore to allow building on windows so that packages for windows don't get reused for *nix --- .dockerignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..39a3205 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +npm-debug.log +.git +.gitignore +logs +data +package-cache +*.md +.env +.env.* From 1b0a4be435785a660e04a5e6becc94f4b7e44d83 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:20:23 +0100 Subject: [PATCH 07/17] heap chart starts at 0 --- stats.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stats.js b/stats.js index 03ec1fb..aad6e74 100644 --- a/stats.js +++ b/stats.js @@ -21,9 +21,10 @@ class ServerStats { const now = Date.now(); const cutoff = now - (24 * 60 * 60 * 1000); // 24 hours ago - // Record memory + // Record memory (limit to 0 minimum) const currentMem = process.memoryUsage().heapUsed; - this.memoryHistory.push({time: now, mem: currentMem - this.startMem}); + const memDelta = Math.max(0, currentMem - this.startMem); + this.memoryHistory.push({time: now, mem: memDelta}); const requestsDelta = this.requestCount - this.requestCountSnapshot; const minutesSinceStart = this.memoryHistory.length > 1 From 391e17ca3615734bfa61c803f6aef025c959cabe Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:23:41 +0100 Subject: [PATCH 08/17] Update dockerfile syntax (remove warning) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b5e93f2..26f42d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app # Install app dependencies COPY package*.json ./ -RUN npm ci --only=production +RUN npm ci --omit=dev # Bundle app source COPY . . From e8e1848694d38755888bedbbe72d4a15b762d562 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:24:42 +0100 Subject: [PATCH 09/17] allow build on windows --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 26f42d9..1d8895d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM node:20-alpine +# Install build tools for native modules (sqlite3, bcrypt) +RUN apk add --no-cache python3 make g++ + # Create app directory WORKDIR /app From 33b41d3b50844c26fa46847d8f79005bfca5d5b9 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:29:23 +0100 Subject: [PATCH 10/17] Fix const stats declaration --- server.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server.js b/server.js index 701e9e9..e5d9e7c 100644 --- a/server.js +++ b/server.js @@ -61,11 +61,10 @@ app.use(express.json({ limit: '50mb' })); // Module instances const modules = {}; -const stats; +const stats = new ServerStats(); // Initialize modules based on configuration async function initializeModules() { - stats = new ServerStats(); // Initialize SHL module if (config.modules.shl.enabled) { From 7c592d0355206ac86a93480e690562c7e07d7b74 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:33:45 +0100 Subject: [PATCH 11/17] restore release workflow --- .github/workflows/release.yml | 224 +++++++++++++++------------------- 1 file changed, 101 insertions(+), 123 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f75dc35..90ec309 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,144 +4,122 @@ on: push: tags: - 'v*.*.*' - workflow_dispatch: - inputs: - version: - description: 'Version to release (e.g., v1.0.0)' - required: true - type: string - -permissions: - contents: write - packages: write jobs: test: name: Test runs-on: ubuntu-latest - + steps: - - uses: actions/checkout@v4 - -# - name: Use Node.js -# uses: actions/setup-node@v4 -# with: -# node-version: '20' -# cache: 'npm' - -# - name: Install dependencies -# run: npm ci - -# - name: Run tests -# run: npm run test:ci -# env: -# SUPPRESS_LOGS: 'true' - -# - name: Upload test results -# uses: actions/upload-artifact@v4 -# if: always() -# with: -# name: test-results -# path: ./test-results/junit.xml - -# - name: Upload coverage reports -# uses: codecov/codecov-action@v4 -# with: -# directory: ./coverage/ -# token: ${{ secrets.CODECOV_TOKEN }} + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm run test:ci + env: + SUPPRESS_LOGS: 'true' + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: ./test-results/junit.xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v4 + with: + directory: ./coverage/ + token: ${{ secrets.CODECOV_TOKEN }} release: name: Create Release runs-on: ubuntu-latest needs: test - + steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version from tag - id: get_version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "VERSION=${{ inputs.version }}" >> $GITHUB_OUTPUT - else - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - fi - - - name: Extract release notes from CHANGELOG.md - id: extract_release_notes - run: | - VERSION=$(echo ${{ steps.get_version.outputs.VERSION }} | sed 's/^v//') - SECTION=$(sed -n "/## \\[v${VERSION}\\]/,/## \\[v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\]/p" CHANGELOG.md | sed '$d') - - if [ -z "$SECTION" ]; then - SECTION="## [v${VERSION}] - $(date +%Y-%m-%d)\nNo detailed release notes available." - fi - - RELEASE_NOTES="${SECTION//'%'/'%25'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" - - echo "RELEASE_NOTES<> $GITHUB_OUTPUT - echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.get_version.outputs.VERSION }} - name: Release ${{ steps.get_version.outputs.VERSION }} - body: ${{ steps.extract_release_notes.outputs.RELEASE_NOTES }} - draft: false - prerelease: false - token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG.md + id: extract_release_notes + run: | + VERSION=$(echo ${{ steps.get_version.outputs.VERSION }} | sed 's/^v//') + # Extract section for current version from CHANGELOG + SECTION=$(sed -n "/## \\[v${VERSION}\\]/,/## \\[v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\]/p" CHANGELOG.md | sed '$d') + + # If no section found, use a default message + if [ -z "$SECTION" ]; then + SECTION="## [v${VERSION}] - $(date +%Y-%m-%d)\nNo detailed release notes available." + fi + + # Escape newlines and special characters for GitHub Actions + RELEASE_NOTES="${SECTION//'%'/'%25'}" + RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" + RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" + + echo "RELEASE_NOTES<> $GITHUB_OUTPUT + echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.get_version.outputs.VERSION }} + name: Release ${{ steps.get_version.outputs.VERSION }} + body: ${{ steps.extract_release_notes.outputs.RELEASE_NOTES }} + draft: false + prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} docker: name: Build and Push Docker Image runs-on: ubuntu-latest needs: release - + steps: - - uses: actions/checkout@v4 - - - name: Get version from tag - id: get_version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - VERSION="${{ inputs.version }}" - else - VERSION=${GITHUB_REF#refs/tags/} - fi - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT - - - name: Set image name (lowercase) - id: image - run: | - IMAGE="ghcr.io/${GITHUB_REPOSITORY}" - echo "IMAGE_LC=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ${{ steps.image.outputs.IMAGE_LC }}:latest - ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION }} - ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION_NO_V }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.get_version.outputs.VERSION_NO_V }} + - uses: actions/checkout@v4 + + - name: Get version from tag + id: get_version + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }} + ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION_NO_V }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ steps.get_version.outputs.VERSION_NO_V }} \ No newline at end of file From cff87317e3386d022b0a8714414bd389cfc52592 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:37:38 +0100 Subject: [PATCH 12/17] better support for repo names that have uppercase characters --- .github/workflows/release.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90ec309..4286201 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,13 @@ jobs: echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT + + - name: Set image name (lowercase) + id: image + run: | + IMAGE="ghcr.io/${GITHUB_REPOSITORY}" + echo "IMAGE_LC=$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -116,9 +123,9 @@ jobs: context: . push: true tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }} - ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION_NO_V }} + ${{ steps.image.outputs.IMAGE_LC }}:latest + ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION }} + ${{ steps.image.outputs.IMAGE_LC }}:${{ steps.get_version.outputs.VERSION_NO_V }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | From 3abdf4136176f5ecfe80c361ccd092e3f403ff49 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:11:34 +0100 Subject: [PATCH 13/17] Fix npm package cache lookup by parsing version from details string --- tx/library.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tx/library.js b/tx/library.js index 5fcdf3c..c71eb41 100644 --- a/tx/library.js +++ b/tx/library.js @@ -391,7 +391,15 @@ class Library { } async loadNpm(packageManager, details, isDefault, mode) { - const packagePath = await packageManager.fetch(details, null); + // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2") + let packageId = details; + let version = null; + if (details.includes('#')) { + const parts = details.split('#'); + packageId = parts[0]; + version = parts[1]; + } + const packagePath = await packageManager.fetch(packageId, version); if (mode === "fetch" || mode === "cs") { return; } From 530002e5747460f234c5cb325ca25d53c3f6ec05 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:38:37 +0100 Subject: [PATCH 14/17] Revert "heap chart starts at 0" This reverts commit 1b0a4be435785a660e04a5e6becc94f4b7e44d83. --- stats.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stats.js b/stats.js index aad6e74..03ec1fb 100644 --- a/stats.js +++ b/stats.js @@ -21,10 +21,9 @@ class ServerStats { const now = Date.now(); const cutoff = now - (24 * 60 * 60 * 1000); // 24 hours ago - // Record memory (limit to 0 minimum) + // Record memory const currentMem = process.memoryUsage().heapUsed; - const memDelta = Math.max(0, currentMem - this.startMem); - this.memoryHistory.push({time: now, mem: memDelta}); + this.memoryHistory.push({time: now, mem: currentMem - this.startMem}); const requestsDelta = this.requestCount - this.requestCountSnapshot; const minutesSinceStart = this.memoryHistory.length > 1 From 7b149716806acced944c030aa7b63239cb63b3e1 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:39:02 +0100 Subject: [PATCH 15/17] fix build --- server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index 5591c4d..9705cfd 100644 --- a/server.js +++ b/server.js @@ -65,7 +65,8 @@ let stats = null; // Initialize modules based on configuration async function initializeModules() { - + stats = new ServerStats(); + // Initialize SHL module if (config.modules.shl.enabled) { try { From c1bdc8d737219ce51591199cb946adb088d27016 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:39:34 +0100 Subject: [PATCH 16/17] add search parameters --- tx/tx-html.js | 14 ++++++++++ tx/tx.js | 18 +++++++++++++ tx/workers/search.js | 61 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/tx/tx-html.js b/tx/tx-html.js index cde4074..ef5453a 100644 --- a/tx/tx-html.js +++ b/tx/tx-html.js @@ -103,6 +103,20 @@ class TxHtmlRenderer { * Check if request accepts HTML */ acceptsHtml(req) { + // Check _format query parameter first (takes precedence) + const format = req.query._format || req.query.format; + if (format) { + const f = format.toLowerCase(); + // If _format specifies json or xml, don't return HTML + if (f === 'json' || f === 'xml' || f.includes('fhir+json') || f.includes('fhir+xml')) { + return false; + } + // Check if _format explicitly requests HTML + if (f === 'html' || f.includes('text/html')) { + return true; + } + } + // Fall back to Accept header const accept = req.headers.accept || ''; return accept.includes('text/html'); } diff --git a/tx/tx.js b/tx/tx.js index abbd8a4..ded17e3 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -66,10 +66,28 @@ class TXModule { } acceptsXml(req) { + // Check _format query parameter first (takes precedence per FHIR spec) + const format = req.query._format || req.query.format; + if (format) { + const f = format.toLowerCase(); + return f === 'xml' || f.includes('fhir+xml') || f.includes('xml+fhir'); + } + // Fall back to Accept header const accept = req.headers.accept || ''; return accept.includes('application/fhir+xml') || accept.includes('application/xml+fhir'); } + acceptsJson(req) { + // Check _format query parameter first + const format = req.query._format || req.query.format; + if (format) { + const f = format.toLowerCase(); + return f === 'json' || f.includes('fhir+json') || f.includes('json+fhir'); + } + // Default to JSON if no specific format requested + return true; + } + /** * Initialize the TX module diff --git a/tx/workers/search.js b/tx/workers/search.js index 9f661d2..4855051 100644 --- a/tx/workers/search.js +++ b/tx/workers/search.js @@ -30,12 +30,15 @@ class SearchWorker extends TerminologyWorker { // Allowed search parameters static ALLOWED_PARAMS = [ - '_offset', '_count', '_elements', '_sort', + '_offset', '_count', '_elements', '_sort', '_summary', '_total', 'url', 'version', 'content-mode', 'date', 'description', 'supplements', 'identifier', 'jurisdiction', 'name', 'publisher', 'status', 'system', 'title', 'text' ]; + // Summary elements per FHIR spec for terminology resources + static SUMMARY_ELEMENTS = ['resourceType', 'id', 'meta', 'url', 'version', 'name', 'title', 'status', 'date', 'publisher', 'description']; + // Sortable fields static SORT_FIELDS = ['id', 'url', 'version', 'date', 'name', 'vurl']; @@ -52,10 +55,26 @@ class SearchWorker extends TerminologyWorker { this.log.debug(`Search ${resourceType} with params:`, params); try { - // Parse pagination parameters + // Parse pagination and control parameters const offset = Math.max(0, parseInt(params._offset) || 0); - const elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null; - const count = Math.min(elements ? 2000 : 200, Math.max(1, parseInt(params._count) || 20)); + const summary = params._summary || 'false'; + const totalMode = params._total || 'accurate'; // accurate, estimate, none + + // Handle _summary parameter - it overrides _elements + let elements = null; + if (summary === 'true') { + elements = SearchWorker.SUMMARY_ELEMENTS; + } else if (summary === 'data') { + // _summary=data means exclude text/narrative - for terminology resources, same as no filter + elements = null; + } else if (summary === 'text') { + // _summary=text means only text element - not very useful for terminology + elements = ['resourceType', 'id', 'meta', 'text']; + } else if (params._elements) { + elements = decodeURIComponent(params._elements).split(',').map(e => e.trim()); + } + + const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, Math.max(1, parseInt(params._count) || 20)); const sort = params._sort || "id"; // Get matching resources @@ -83,9 +102,9 @@ class SearchWorker extends TerminologyWorker { // Build and return the bundle const bundle = this.buildSearchBundle( - req, resourceType, matches, offset, count, elements + req, resourceType, matches, offset, count, elements, summary, totalMode ); - req.logInfo = `${bundle.entry.length} matches`; + req.logInfo = summary === 'count' ? `count: ${bundle.total}` : `${bundle.entry.length} matches`; return res.json(bundle); } catch (error) { @@ -265,10 +284,27 @@ class SearchWorker extends TerminologyWorker { /** * Build a FHIR search Bundle with pagination + * @param {Object} req - Express request + * @param {string} resourceType - Resource type + * @param {Array} allMatches - All matching resources + * @param {number} offset - Pagination offset + * @param {number} count - Page size + * @param {Array} elements - Elements to include (or null for all) + * @param {string} summary - _summary parameter value + * @param {string} totalMode - _total parameter value (accurate, estimate, none) */ - buildSearchBundle(req, resourceType, allMatches, offset, count, elements) { + buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary = 'false', totalMode = 'accurate') { const total = allMatches.length; + // Handle _summary=count - return only count, no entries + if (summary === 'count') { + return { + resourceType: 'Bundle', + type: 'searchset', + total: total + }; + } + // Get the slice for this page const pageResults = allMatches.slice(offset, offset + count); @@ -352,13 +388,20 @@ class SearchWorker extends TerminologyWorker { }; }); - return { + // Build result bundle + const bundle = { resourceType: 'Bundle', type: 'searchset', - total: total, link: links, entry: entries }; + + // Include total unless _total=none + if (totalMode !== 'none') { + bundle.total = total; + } + + return bundle; } /** From 540fcab9cf59c8a68c6510a68a6d7208de3823b0 Mon Sep 17 00:00:00 2001 From: Jose Costa Teixeira <16153168+costateixeira@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:58:44 +0100 Subject: [PATCH 17/17] add tests for new search parameters --- tests/tx/search.test.js | 119 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/tx/search.test.js b/tests/tx/search.test.js index 82e1be2..0a7af62 100644 --- a/tests/tx/search.test.js +++ b/tests/tx/search.test.js @@ -200,4 +200,123 @@ describe('Search Worker', () => { } }); }); + describe('_summary parameter', () => { + test('should return only summary elements with _summary=true', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _summary: 'true', _count: 5 }) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body.resourceType).toBe('Bundle'); + + if (response.body.entry && response.body.entry.length > 0) { + const resource = response.body.entry[0].resource; + // Summary elements should be present + expect(resource.resourceType).toBe('CodeSystem'); + expect(resource.id).toBeDefined(); + // Non-summary elements should be absent + expect(resource.concept).toBeUndefined(); + expect(resource.property).toBeUndefined(); + } + }); + + test('should return only count with _summary=count', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _summary: 'count' }) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body.resourceType).toBe('Bundle'); + expect(response.body.type).toBe('searchset'); + expect(response.body.total).toBeGreaterThan(0); + // No entries when _summary=count + expect(response.body.entry).toBeUndefined(); + expect(response.body.link).toBeUndefined(); + }); + + test('should return full resources with _summary=false', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _summary: 'false', url: 'http://hl7.org/fhir/administrative-gender' }) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + + if (response.body.entry && response.body.entry.length > 0) { + const resource = response.body.entry[0].resource; + expect(resource.resourceType).toBe('CodeSystem'); + // Full resource should include concept + expect(resource.concept).toBeDefined(); + } + }); + }); + + describe('_total parameter', () => { + test('should include total with _total=accurate (default)', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _count: 5 }) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body.total).toBeDefined(); + expect(typeof response.body.total).toBe('number'); + }); + + test('should not include total with _total=none', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _total: 'none', _count: 5 }) + .set('Accept', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body.resourceType).toBe('Bundle'); + expect(response.body.total).toBeUndefined(); + }); + }); + + describe('_format parameter', () => { + test('should return JSON with _format=json', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _format: 'json', _count: 2 }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toContain('application/fhir+json'); + expect(response.body.resourceType).toBe('Bundle'); + }); + + test('should return XML with _format=xml', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _format: 'xml', _count: 2 }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toContain('application/fhir+xml'); + expect(response.text).toContain(' { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _format: 'application/fhir+json', _count: 2 }); + + expect(response.status).toBe(200); + expect(response.headers['content-type']).toContain('application/fhir+json'); + }); + + test('_format should override Accept header', async () => { + const response = await request(app) + .get('/tx/r5/CodeSystem') + .query({ _format: 'xml', _count: 2 }) + .set('Accept', 'application/fhir+json'); + + expect(response.status).toBe(200); + // _format=xml should override Accept: application/fhir+json + expect(response.headers['content-type']).toContain('application/fhir+xml'); + }); + }); });