diff --git a/.env.example b/.env.example index 085f748..af92864 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,6 @@ WEB_ORIGIN=https://fieldlens.example.invalid FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0 FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download FIELDLENS_PRINT_DOWNLOAD_DIR=downloads +FIELDLENS_DESKTOP_RELEASE_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/releases/download +FIELDLENS_DESKTOP_RELEASE_TAG=v0.1.0 +FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads diff --git a/.gitea/workflows/desktop-release.yml b/.gitea/workflows/desktop-release.yml index f9546c9..4721676 100644 --- a/.gitea/workflows/desktop-release.yml +++ b/.gitea/workflows/desktop-release.yml @@ -1,202 +1,10 @@ -name: Desktop Release +name: Desktop Release (moved) on: - push: - branches: [main] - tags: - - "fieldlens-print-v*" - paths: - - "apps/print/**" - - ".gitea/workflows/desktop-release.yml" - pull_request: - paths: - - "apps/print/**" - - ".gitea/workflows/desktop-release.yml" workflow_dispatch: -permissions: - code: read - releases: read - jobs: - windows-nsis: - name: Windows x64 NSIS + moved: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: x86_64-pc-windows-msvc - - - name: Install Windows cross-build tools - run: | - sudo apt-get update - sudo apt-get install -y nsis lld llvm clang - cargo install --locked cargo-xwin - - - name: Install Node dependencies - run: npm install - - - name: Build Windows NSIS installer - run: npm run bundle:windows --workspace @fieldlens/print - env: - XWIN_CACHE_DIR: ${{ runner.temp }}/xwin-cache - - - name: Stage Windows installer - shell: bash - run: | - set -euo pipefail - source_file="$(find apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name '*-setup.exe' -print -quit)" - test -n "$source_file" - mkdir -p dist/desktop - cp "$source_file" dist/desktop/fieldlens-print-windows-x64-setup.exe - - - name: Upload Windows installer - uses: actions/upload-artifact@v3 - with: - name: fieldlens-print-windows-x64 - path: dist/desktop/fieldlens-print-windows-x64-setup.exe - if-no-files-found: error - retention-days: 14 - - linux-deb: - name: Linux x64 DEB - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 24 - - - uses: dtolnay/rust-toolchain@stable - - - name: Install Tauri Linux prerequisites - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential \ - curl \ - wget \ - file \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - libssl-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - patchelf - - - name: Install Node dependencies - run: npm install - - - name: Build Debian package - run: npm run bundle:deb --workspace @fieldlens/print - - - name: Stage Debian package - shell: bash - run: | - set -euo pipefail - source_file="$(find apps/print/src-tauri/target/release/bundle/deb -maxdepth 1 -type f -name '*.deb' -print -quit)" - test -n "$source_file" - mkdir -p dist/desktop - cp "$source_file" dist/desktop/fieldlens-print-linux-x64.deb - - - name: Upload Debian package - uses: actions/upload-artifact@v3 - with: - name: fieldlens-print-linux-x64-deb - path: dist/desktop/fieldlens-print-linux-x64.deb - if-no-files-found: error - retention-days: 14 - - publish-release: - name: Publish tagged desktop release - if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/fieldlens-print-v') - needs: [windows-nsis, linux-deb] - runs-on: ubuntu-latest - permissions: - code: read - releases: write - steps: - - name: Download Windows installer - uses: actions/download-artifact@v3 - with: - name: fieldlens-print-windows-x64 - path: release-assets - - - name: Download Debian package - uses: actions/download-artifact@v3 - with: - name: fieldlens-print-linux-x64-deb - path: release-assets - - - name: Publish Gitea release assets - shell: bash - env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - GITEA_SERVER_URL: ${{ gitea.server_url }} - GITEA_REPOSITORY: ${{ gitea.repository }} - GITEA_REF: ${{ gitea.ref }} - GITEA_REF_NAME: ${{ gitea.ref_name }} - GITEA_SHA: ${{ gitea.sha }} - run: | - set -euo pipefail - - api="${GITEA_SERVER_URL%/}/api/v1/repos/${GITEA_REPOSITORY}" - auth_header="Authorization: token ${GITEA_TOKEN}" - release_tag="$GITEA_REF_NAME" - if [ "$GITEA_REF" = "refs/heads/main" ]; then - release_tag="fieldlens-print-v0.1.0" - fi - - status="$(curl -sS -o /tmp/release.json -w '%{http_code}' \ - -H "$auth_header" \ - "$api/releases/tags/$release_tag")" - - if [ "$status" = "404" ]; then - jq -n \ - --arg tag "$release_tag" \ - --arg sha "$GITEA_SHA" \ - '{ - tag_name: $tag, - target_commitish: $sha, - name: ("FieldLens Print " + ($tag | sub("^fieldlens-print-"; ""))), - body: "FieldLens Print desktop installers.", - draft: false, - prerelease: false - }' > /tmp/create-release.json - - curl -fsS \ - -X POST \ - -H "$auth_header" \ - -H "Content-Type: application/json" \ - --data-binary @/tmp/create-release.json \ - "$api/releases" > /tmp/release.json - elif [ "$status" != "200" ]; then - cat /tmp/release.json - exit 1 - fi - - release_id="$(jq -r '.id' /tmp/release.json)" - test "$release_id" != "null" - - curl -fsS -H "$auth_header" "$api/releases/$release_id/assets" > /tmp/assets.json - - for file in release-assets/fieldlens-print-windows-x64-setup.exe release-assets/fieldlens-print-linux-x64.deb; do - name="$(basename "$file")" - existing_id="$(jq -r --arg name "$name" '.[] | select(.name == $name) | .id' /tmp/assets.json | head -n 1)" - if [ -n "$existing_id" ] && [ "$existing_id" != "null" ]; then - curl -fsS -X DELETE -H "$auth_header" "$api/releases/$release_id/assets/$existing_id" - fi - - curl -fsS \ - -X POST \ - -H "$auth_header" \ - -F "attachment=@$file" \ - "$api/releases/$release_id/assets?name=$name" - done + - run: echo "Desktop builds now run from Admin/fieldlens-desktop" diff --git a/Dockerfile b/Dockerfile index d963268..a4393e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,3 @@ -FROM node:24-bookworm AS desktop-build -WORKDIR /workspace - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential \ - clang \ - curl \ - file \ - libayatana-appindicator3-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - libxdo-dev \ - lld \ - llvm \ - nsis \ - patchelf \ - pkg-config \ - librsvg2-dev \ - wget \ - && rm -rf /var/lib/apt/lists/* - -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal -ENV PATH="/root/.cargo/bin:$PATH" - -RUN rustup target add x86_64-pc-windows-msvc \ - && cargo install --locked cargo-xwin - -COPY package.json ./ -COPY apps/api/package.json apps/api/package.json -COPY apps/web/package.json apps/web/package.json -COPY apps/print/package.json apps/print/package.json -COPY packages/contracts/package.json packages/contracts/package.json -COPY packages/fieldlens-core/package.json packages/fieldlens-core/package.json - -RUN npm install - -COPY apps/print apps/print - -RUN npm run bundle:windows --workspace @fieldlens/print \ - && npm run bundle:deb --workspace @fieldlens/print \ - && mkdir -p /desktop-artifacts \ - && windows_file="$(find apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis -maxdepth 1 -type f -name '*-setup.exe' -print -quit)" \ - && linux_file="$(find apps/print/src-tauri/target/release/bundle/deb -maxdepth 1 -type f -name '*.deb' -print -quit)" \ - && test -n "$windows_file" \ - && test -n "$linux_file" \ - && cp "$windows_file" /desktop-artifacts/fieldlens-print-windows-x64-setup.exe \ - && cp "$linux_file" /desktop-artifacts/fieldlens-print-linux-x64.deb - FROM node:24-alpine AS build WORKDIR /workspace @@ -81,7 +31,6 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 ENV STATIC_DIR=/app/web -ENV FIELDLENS_PRINT_DOWNLOAD_DIR=/app/downloads COPY --from=build /workspace/package.json /app/package.json COPY --from=build /workspace/node_modules /app/node_modules @@ -91,7 +40,6 @@ COPY --from=build /workspace/migrations /app/migrations COPY --from=build /workspace/packages/contracts/package.json /app/packages/contracts/package.json COPY --from=build /workspace/packages/contracts/dist /app/packages/contracts/dist COPY --from=build /workspace/apps/web/dist /app/web -COPY --from=desktop-build /desktop-artifacts /app/downloads USER node EXPOSE 3000 diff --git a/README.md b/README.md index f8f04af..fb01834 100644 --- a/README.md +++ b/README.md @@ -62,20 +62,18 @@ The Windows NSIS bundle is configured for `currentUser` installation, so install ### Desktop release artifacts -The `.gitea/workflows/desktop-release.yml` workflow packages FieldLens Print on changes to the desktop app, on `main`, and on `fieldlens-print-v*` tags. +Desktop packaging is intentionally separate from the PWA/API deployment. -Artifacts: +- Private source of truth: `Admin/fieldlens/apps/print` +- Public desktop build/release mirror: `Admin/fieldlens-desktop` +- PWA/API deploy mirror: `Admin/fieldlens-deploy` -- **Windows x64:** NSIS current-user installer from `apps/print/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe` -- **Linux x64:** Debian package from `apps/print/src-tauri/target/release/bundle/deb/*.deb` +The desktop mirror builds Windows and Linux installers with Gitea Actions and publishes public release assets. Normal FieldLens web/API deployments do not install Rust, Tauri, cargo-xwin, NSIS, or Linux desktop packaging dependencies. -After a successful Gitea Actions run, download the corresponding `fieldlens-print-windows-x64` or `fieldlens-print-linux-x64-deb` artifact from the workflow run. +Current public release assets: -Local packaging commands: +- `fieldlens-print-windows-x64-setup.exe` +- `fieldlens-print-linux-x64.deb` -```bash -npm run bundle:windows --workspace @fieldlens/print -npm run bundle:deb --workspace @fieldlens/print -``` +The FieldLens `/downloads` page keeps stable same-site links; the API redirects those links to the configured desktop release tag. -The Windows package is cross-compiled with `cargo-xwin` and bundled with NSIS. MSI packaging is intentionally not part of the Linux runner workflow because WiX/MSI bundling requires Windows. diff --git a/apps/api/.env.example b/apps/api/.env.example index ac229a3..cc7d281 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -14,3 +14,6 @@ STATIC_DIR= FIELDLENS_PRINT_RELEASE_TAG=fieldlens-print-v0.1.0 FIELDLENS_PRINT_RELEASE_BASE_URL=https://git.example.invalid/owner/fieldlens/releases/download FIELDLENS_PRINT_DOWNLOAD_DIR=downloads +FIELDLENS_DESKTOP_RELEASE_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/releases/download +FIELDLENS_DESKTOP_RELEASE_TAG=v0.1.0 +FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL=https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 4caf3e0..ff6c3d4 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -18,5 +18,5 @@ export const config = { logLevel: process.env.LOG_LEVEL?.trim() || 'info', webOrigin: process.env.WEB_ORIGIN?.trim() || 'http://localhost:5173', staticDir: process.env.STATIC_DIR?.trim() || '', - printDownloadDir: process.env.FIELDLENS_PRINT_DOWNLOAD_DIR?.trim() || 'downloads', + desktopDownloadBaseUrl: process.env.FIELDLENS_DESKTOP_DOWNLOAD_BASE_URL?.trim() || 'https://git.arrowstack.dev/Admin/fieldlens-desktop/raw/branch/main/downloads', } diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index ae9869c..691dee2 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,6 +1,6 @@ import path from 'node:path' import cors from 'cors' -import express, { type Response } from 'express' +import express from 'express' import helmet from 'helmet' import { FIELDLENS_SCHEMA_VERSION, isSyncRequest, type DeviceRegistration } from '@fieldlens/contracts' import { getAccessRequest, listAccessRequests, requestAccess, reviewAccessRequest } from './access.js' @@ -61,33 +61,27 @@ app.get('/api/health/live', (_req, res) => { res.json({ status: 'live', version: process.env.FIELDLENS_VERSION ?? 'dev' }) }) -const desktopDownloadDir = path.resolve(config.printDownloadDir) - -function sendDesktopDownload(res: Response, filename: string) { - const file = path.join(desktopDownloadDir, filename) - res.setHeader('Cache-Control', 'public, max-age=300') - res.download(file, filename, (error) => { - if (!error || res.headersSent) return - log('error', 'desktop_download_missing', { filename, file }) - res.status(404).json({ error: 'desktop_download_unavailable' }) - }) +function redirectDesktopDownload(res: express.Response, filename: string) { + const base = config.desktopDownloadBaseUrl.replace(/\/$/, '') + res.setHeader('Cache-Control', 'no-store') + res.redirect(302, `${base}/${filename}`) } app.get('/downloads/fieldlens-print-windows-x64-setup.exe', (_req, res) => { - sendDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') + redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') }) app.get('/downloads/fieldlens-print-linux-x64.deb', (_req, res) => { - sendDesktopDownload(res, 'fieldlens-print-linux-x64.deb') + redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb') }) // Backward-compatible aliases used by the first downloads page. app.get('/downloads/fieldlens-print/windows-x64', (_req, res) => { - sendDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') + redirectDesktopDownload(res, 'fieldlens-print-windows-x64-setup.exe') }) app.get('/downloads/fieldlens-print/linux-x64.deb', (_req, res) => { - sendDesktopDownload(res, 'fieldlens-print-linux-x64.deb') + redirectDesktopDownload(res, 'fieldlens-print-linux-x64.deb') }) app.get('/api/health/ready', async (_req, res) => { diff --git a/apps/web/src/DesktopDownloads.tsx b/apps/web/src/DesktopDownloads.tsx index 305b553..47ba112 100644 --- a/apps/web/src/DesktopDownloads.tsx +++ b/apps/web/src/DesktopDownloads.tsx @@ -15,7 +15,7 @@ const downloads = [ }, ] -export function DesktopDownloadsLink() { +export function DesktopDownloadsLink({ onOpen }: { onOpen: () => void }) { return (
@@ -23,22 +23,22 @@ export function DesktopDownloadsLink() {

Desktop app

Capture printed barcodes without a physical scan

- +
) } -export function DesktopDownloadsPage() { +export function DesktopDownloadsPage({ onBack }: { onBack: () => void }) { return (
- Back to FieldLens +
FL diff --git a/apps/web/src/host.css b/apps/web/src/host.css index 47e6135..3df403a 100644 --- a/apps/web/src/host.css +++ b/apps/web/src/host.css @@ -235,3 +235,23 @@ .downloads-panel-heading { padding: 18px 17px 15px; } .downloads-notes { padding: 13px 16px 16px; } } + + +.host-app-behind-page { + display: none; +} + +.desktop-download-settings-link { + width: calc(100% - 36px); + cursor: pointer; + text-align: left; + font: inherit; +} + +.downloads-back { + border: 0; + padding: 0; + background: transparent; + cursor: pointer; + font: inherit; +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index fd4ea26..dac3e47 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,4 +1,4 @@ -import { StrictMode } from 'react' +import { StrictMode, useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' import '../../../packages/fieldlens-core/src/index.css' @@ -19,15 +19,62 @@ startDeviceHoldTracking() registerArrowstackReferenceProvider() installSyncTriggers() -const isDownloadsPage = window.location.pathname === '/downloads' -const root = createRoot(document.getElementById('root')!) +function HostApp() { + const initialDownloadsPage = window.location.pathname === '/downloads' + const [downloadsOpen, setDownloadsOpen] = useState(initialDownloadsPage) + const [appMounted, setAppMounted] = useState(!initialDownloadsPage) -root.render( + useEffect(() => { + const onPopState = () => { + const open = window.location.pathname === '/downloads' + setDownloadsOpen(open) + if (!open) setAppMounted(true) + } + + window.addEventListener('popstate', onPopState) + return () => window.removeEventListener('popstate', onPopState) + }, []) + + const openDownloads = () => { + setAppMounted(true) + if (window.location.pathname !== '/downloads') { + window.history.pushState({ fieldlensPage: 'downloads' }, '', '/downloads') + } + setDownloadsOpen(true) + } + + const closeDownloads = () => { + if (window.location.pathname === '/downloads' && window.history.length > 1) { + window.history.back() + return + } + + window.history.replaceState({}, '', '/') + setDownloadsOpen(false) + setAppMounted(true) + } + + return ( + <> + {appMounted && ( +
+ + + + + )} + /> +
+ )} + {downloadsOpen && } + + ) +} + +createRoot(document.getElementById('root')!).render( - {isDownloadsPage ? ( - - ) : ( - } /> - )} + , )