Compare commits
130 Commits
v1.0.0
...
renovate/h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8dcbca22 | ||
| db82c15c51 | |||
|
ec38a69c57
|
|||
|
ae08e4dc5f
|
|||
|
cd3d9862c3
|
|||
|
0f383d7f44
|
|||
|
1180804025
|
|||
|
c22e1af86f
|
|||
|
09c62bed71
|
|||
|
c780fe040a
|
|||
|
1e521b0c59
|
|||
|
832afe7b90
|
|||
|
6d0069a8d3
|
|||
|
12e3cf9354
|
|||
|
7ba1cfe6f7
|
|||
| 15d697c946 | |||
|
a8a4405946
|
|||
|
2ddd0bf9fd
|
|||
| c3e10b3945 | |||
|
6da7b31269
|
|||
|
4b553d67d4
|
|||
| 3b5807a480 | |||
|
e6c0387cdd
|
|||
| ce65f05bd4 | |||
|
3190c6f119
|
|||
|
7a725a505f
|
|||
| 6a1667b34d | |||
|
2f1bf6a05a
|
|||
|
3662bda009
|
|||
|
0757ca64f9
|
|||
|
d2856b27a8
|
|||
|
1532bcae31
|
|||
|
c9627a71ea
|
|||
|
8ed205375b
|
|||
|
0bf731df66
|
|||
|
2064760ea9
|
|||
|
06300f08e3
|
|||
|
c463eb1d94
|
|||
|
add7f6e530
|
|||
|
7a3b3ca054
|
|||
|
540f9712db
|
|||
|
6ac2968b73
|
|||
|
217b10b1fd
|
|||
|
3b6331faea
|
|||
|
8eb12a7087
|
|||
|
5fac643e86
|
|||
|
d862b1d222
|
|||
|
46bc6bdb22
|
|||
|
0b90005bf7
|
|||
|
bab846cd83
|
|||
|
59030ba2a3
|
|||
|
9468010981
|
|||
|
38887b1de2
|
|||
|
2c65a17b12
|
|||
|
c1c823d2b1
|
|||
|
9e7a9f6d2c
|
|||
|
a3a78ae117
|
|||
|
51ce1cbc40
|
|||
|
1202652e93
|
|||
|
3b9b8e0a65
|
|||
|
44ccc672fc
|
|||
|
625dcc11f8
|
|||
|
62f3f34e10
|
|||
|
aad59ffe43
|
|||
|
4b20bf540e
|
|||
|
dd5e24ae26
|
|||
|
bc20b06fe1
|
|||
|
e7801735fa
|
|||
|
fbbb6a5e9c
|
|||
|
ebdbd02599
|
|||
|
312fe5e746
|
|||
|
469b59a561
|
|||
|
bc5b12b23c
|
|||
|
5f5de2272b
|
|||
|
7b4598c5ca
|
|||
|
ab2dd4e4ed
|
|||
|
|
e366ba8f1b | ||
|
|
6971796c00 | ||
|
|
d85d960e3d | ||
|
|
d99fce9b24 | ||
|
|
d770914aae | ||
|
|
02d745f9ae | ||
|
|
5c7c6c4ca5 | ||
|
|
e6bc79ec27 | ||
|
03f0c71aae
|
|||
|
4923619b51
|
|||
|
7e75064ae5
|
|||
|
7c13aee0a7
|
|||
|
d5b37ed53f
|
|||
|
bbc4fd4c32
|
|||
|
06f3e6fa5a
|
|||
|
548d5dbc35
|
|||
|
78c07e1c6b
|
|||
|
4afe001117
|
|||
|
826e7d10d1
|
|||
|
4646423f1d
|
|||
|
a3a8e29a6d
|
|||
|
a2411bd176
|
|||
|
2465e2e42b
|
|||
|
3d81697538
|
|||
|
5896cd7064
|
|||
|
9ffce1e12f
|
|||
|
b4c65bf30b
|
|||
|
7d2eb81e0f
|
|||
|
837c5c471d
|
|||
|
4b9a972706
|
|||
|
9dd65dbff8
|
|||
|
5eb10386de
|
|||
|
c3b0173da3
|
|||
|
0a4a5f7634
|
|||
|
ecc1253937
|
|||
|
de392d52ea
|
|||
|
204dceeff7
|
|||
|
410448b35d
|
|||
|
119177d64c
|
|||
|
df9ed9465b
|
|||
|
c2de35082f
|
|||
|
bc63b4a42c
|
|||
|
8d4e8cde81
|
|||
|
c9053cb0c6
|
|||
|
8d2d520122
|
|||
|
|
d6d8e8240f | ||
|
|
a16e96355f | ||
|
ea21931650
|
|||
|
90de7a4850
|
|||
|
0c9db82791
|
|||
|
fa4ff7444d
|
|||
|
8d82c160d1
|
|||
|
99d94e092d
|
|||
|
003a88dcee
|
@@ -1,14 +1,32 @@
|
||||
node_modules
|
||||
# Dependencies
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
# SvelteKit & Vite
|
||||
.svelte-kit/
|
||||
build/
|
||||
dist/
|
||||
.output/
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# OS
|
||||
# Go & Binaries
|
||||
bin/
|
||||
linking-tool
|
||||
tmp/
|
||||
|
||||
# Wails Desktop
|
||||
desktop/frontend_dist/
|
||||
desktop/build/
|
||||
wailsjs/
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE & OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -16,8 +34,3 @@ Thumbs.db
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
108
.gitea/workflows/build.yml
Normal file
108
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,108 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@a5b3063b1edaa6ba4911c8a1b1d5e1656fba3ea5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Setup environment
|
||||
run: task setup
|
||||
|
||||
- name: Install dependencies
|
||||
run: task install:ci
|
||||
|
||||
- name: Build frontend
|
||||
run: task build:frontend
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
- name: Build server binaries
|
||||
run: |
|
||||
task build:backend
|
||||
task build-linux-amd64
|
||||
task build-windows-amd64
|
||||
|
||||
- name: Build desktop Linux
|
||||
run: task desktop-linux
|
||||
|
||||
- name: Build desktop Windows
|
||||
run: task desktop-windows
|
||||
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
mkdir -p release-assets
|
||||
cp bin/linking-tool-linux-amd64 release-assets/
|
||||
cp bin/linking-tool-windows-amd64.exe release-assets/
|
||||
if [ -f desktop/build/bin/linking-tool ]; then
|
||||
cp desktop/build/bin/linking-tool release-assets/linking-tool-desktop-linux-amd64
|
||||
fi
|
||||
if [ -f desktop/build/bin/linking-tool.exe ]; then
|
||||
cp desktop/build/bin/linking-tool.exe release-assets/linking-tool-desktop-windows-amd64.exe
|
||||
fi
|
||||
|
||||
- name: Download SBOM
|
||||
run: |
|
||||
git fetch origin master:master || true
|
||||
git checkout master -- sbom/ || git checkout ${{ github.sha }} -- sbom/ || true
|
||||
if [ -d sbom ]; then
|
||||
cp sbom/*.json release-assets/ || true
|
||||
fi
|
||||
|
||||
- name: Create Release
|
||||
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74 # v1
|
||||
with:
|
||||
api_url: ${{ secrets.GITEA_API_URL }}
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }}
|
||||
title: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
Release ${{ steps.version.outputs.version }}
|
||||
|
||||
## Assets
|
||||
- Server binaries (Linux AMD64, Windows AMD64)
|
||||
- Desktop applications (Linux AMD64, Windows AMD64)
|
||||
- SBOM files
|
||||
files: release-assets/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
@@ -7,33 +7,66 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache: pnpm
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
- name: Setup environment
|
||||
run: task setup
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Svelte check (fail on warnings)
|
||||
run: bash scripts/check.sh
|
||||
run: task install:ci
|
||||
- name: Lint
|
||||
run: task lint
|
||||
- name: Frontend checks
|
||||
run: task check
|
||||
- name: Build frontend
|
||||
run: task build:frontend
|
||||
- name: Upload frontend assets
|
||||
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
|
||||
with:
|
||||
name: frontend-build
|
||||
path: build/
|
||||
|
||||
build:
|
||||
scan-backend:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: bash scripts/build.sh
|
||||
go-version: '1.25.5'
|
||||
- name: Run gosec security scan
|
||||
uses: https://git.quad4.io/actions/gosec@424fc4cd9c82ea0fd6bee9cd49c2db2c3cc0c93f # v2.22.11
|
||||
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-frontend, scan-backend]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Download frontend assets
|
||||
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: frontend-build
|
||||
path: build/
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
- name: Build backend
|
||||
run: task build:backend
|
||||
|
||||
64
.gitea/workflows/docker.yml
Normal file
64
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Build and Publish Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
REGISTRY: git.quad4.io
|
||||
IMAGE_NAME: quad4-software/linking-tool
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
outputs:
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
image_tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: https://git.quad4.io/actions/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
with:
|
||||
platforms: amd64,arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: https://git.quad4.io/actions/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: https://git.quad4.io/actions/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: build
|
||||
uses: https://git.quad4.io/actions/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -14,7 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: OSV scan
|
||||
run: bash scripts/osv_scan.sh
|
||||
|
||||
@@ -14,7 +14,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: OSV scan
|
||||
run: bash scripts/osv_scan.sh
|
||||
|
||||
63
.gitea/workflows/sbom.yml
Normal file
63
.gitea/workflows/sbom.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Generate SBOM
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-sbom:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: https://git.quad4.io/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
- name: Setup Task
|
||||
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
|
||||
with:
|
||||
version: '3.46.3'
|
||||
|
||||
- name: Setup environment
|
||||
run: task setup
|
||||
|
||||
- name: Install dependencies
|
||||
run: task install:ci
|
||||
|
||||
- name: Download Trivy
|
||||
run: |
|
||||
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
|
||||
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
|
||||
|
||||
- name: Generate SBOM
|
||||
run: |
|
||||
mkdir -p sbom
|
||||
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
|
||||
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
|
||||
|
||||
- name: Commit and Push Changes
|
||||
run: |
|
||||
git config --global user.name "Gitea Action"
|
||||
git config --global user.email "actions@noreply.quad4.io"
|
||||
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
|
||||
git fetch origin master
|
||||
git checkout master
|
||||
git add sbom/
|
||||
git diff --quiet && git diff --staged --quiet || (git commit -m "Auto-update SBOM [skip ci]" && git push origin master)
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
40
.gitignore
vendored
40
.gitignore
vendored
@@ -1,23 +1,35 @@
|
||||
node_modules
|
||||
# Dependencies
|
||||
node_modules/
|
||||
vendor/
|
||||
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
# SvelteKit & Vite
|
||||
.svelte-kit/
|
||||
build/
|
||||
dist/
|
||||
.output/
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
# OS
|
||||
# Go & Binaries
|
||||
bin/
|
||||
linking-tool
|
||||
tmp/
|
||||
|
||||
# Wails Desktop
|
||||
desktop/frontend_dist/
|
||||
desktop/build/bin/
|
||||
desktop/build/
|
||||
wailsjs/
|
||||
|
||||
# IDE & OS
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
3
.npmrc
3
.npmrc
@@ -1 +1,2 @@
|
||||
engine-strict=true
|
||||
@quad4:registry=https://git.quad4.io/api/packages/quad4-software/npm/
|
||||
//git.quad4.io/api/packages/quad4-software/npm/:_authToken=${NPM_TOKEN}
|
||||
57
CHANGELOG.md
Normal file
57
CHANGELOG.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## 1.5.1 - 2025-12-29
|
||||
|
||||
### Features
|
||||
|
||||
- Added HOST environment variable support for configuring server host binding
|
||||
|
||||
### Security
|
||||
|
||||
- Fixed unhandled error in HTTP response writing (G104)
|
||||
- Fixed file write permissions to use more restrictive 0600 instead of 0644 (G306)
|
||||
- Fixed potential file inclusion vulnerability by adding path validation in file operations (G304)
|
||||
|
||||
### Docker
|
||||
|
||||
- Added HOST environment variable to Dockerfile (defaults to 0.0.0.0, make sure to set it properly in production)
|
||||
|
||||
## 1.5.0 - 2025-12-29
|
||||
|
||||
### Features
|
||||
|
||||
- Move to IndexedDB for saving graph data (from localStorage)
|
||||
- Add multiple links support between nodes.
|
||||
- Increase undo/redo history to 100 steps.
|
||||
- Move undo/redo operations to IndexedDB instead of memory.
|
||||
- Mass selection improvements (moving and linking multiple nodes at once).
|
||||
- Codebase refactor to use Svelte 5 Runes.
|
||||
- Mobile improvements
|
||||
- Added SBOM generation, see `/sbom/` for the generated SBOMs.
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
- `@sveltejs/kit`: ^2.49.1 -> ^2.49.2
|
||||
- `@typescript-eslint/eslint-plugin`: ^8.50.1 -> ^8.51.0
|
||||
- `@typescript-eslint/parser`: ^8.50.1 -> ^8.51.0
|
||||
- `svelte`: ^5.45.6 -> ^5.46.1
|
||||
- `svelte-check`: ^4.3.4 -> ^4.3.5
|
||||
- `vite`: ^7.2.6 -> ^7.3.0
|
||||
- Added `eslint-plugin-security`: ^3.0.1
|
||||
|
||||
|
||||
### Major Codebase Changes
|
||||
|
||||
- Moved from `npm` to `pnpm`
|
||||
- Updated license from `MIT` to `BSD-3-Clause`
|
||||
- Moved from `Makefile` to `Taskfile`
|
||||
- Codebase organization and structure changes
|
||||
|
||||
### CI/CD Updates
|
||||
|
||||
- Updated CI workflows to use `task` commands instead of bash scripts
|
||||
- Added gosec security scanning to backend build pipeline
|
||||
|
||||
### Security
|
||||
|
||||
- Overrode `cookie` package to latest version (1.1.1) due to low severity vulnerability in default version.
|
||||
33
Dockerfile
33
Dockerfile
@@ -1,33 +0,0 @@
|
||||
FROM cgr.dev/chainguard/node:latest-dev AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --chown=node:node package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
RUN npm install --save-dev @sveltejs/adapter-node@latest
|
||||
|
||||
COPY --chown=node:node . .
|
||||
COPY --chown=node:node svelte.config.docker.js svelte.config.js
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM cgr.dev/chainguard/node:latest AS runtime
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder --chown=node:node /app/package.json /app/package-lock.json ./
|
||||
RUN npm install --omit=dev && \
|
||||
npm cache clean --force
|
||||
|
||||
COPY --from=builder --chown=node:node /app/build ./build
|
||||
COPY --from=builder --chown=node:node /app/package.json ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
CMD ["build/index.js"]
|
||||
|
||||
38
LICENSE
38
LICENSE
@@ -1,22 +1,28 @@
|
||||
MIT License
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025 Quad4.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
32
Makefile
32
Makefile
@@ -1,32 +0,0 @@
|
||||
.PHONY: help install dev build preview check lint format clean
|
||||
|
||||
help:
|
||||
@echo 'Usage: make [target]'
|
||||
@echo ''
|
||||
@echo 'Available targets:'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
install:
|
||||
npm install
|
||||
|
||||
dev:
|
||||
npm run dev
|
||||
|
||||
build:
|
||||
npm run build
|
||||
|
||||
preview:
|
||||
npm run preview
|
||||
|
||||
check:
|
||||
npm run check
|
||||
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
format:
|
||||
npm run format
|
||||
|
||||
clean:
|
||||
rm -rf .svelte-kit build node_modules/.vite
|
||||
|
||||
139
README.md
139
README.md
@@ -1,43 +1,142 @@
|
||||
# Quad4 Linking Tool
|
||||
|
||||
A client-side identity graph visualization tool for mapping relationships between entities.
|
||||
A web linking tool for mapping relationships between entities.
|
||||
|
||||
<img src="showcase/linkingtool.png" alt="showcase image" width="900">
|
||||
|
||||
Desktop apps for Windows, macOS, and Linux are coming soon...
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using the Binary
|
||||
|
||||
1. Build the binary:
|
||||
|
||||
```sh
|
||||
task build
|
||||
```
|
||||
|
||||
2. Run the server:
|
||||
|
||||
```sh
|
||||
./bin/linking-tool --port 8080
|
||||
```
|
||||
|
||||
3. Open your browser at `http://localhost:8080`
|
||||
|
||||
### Using Docker
|
||||
|
||||
```sh
|
||||
docker run -p 8080:8080 git.quad4.io/quad4-software/linking-tool
|
||||
```
|
||||
|
||||
Then open your browser at `http://localhost:8080`
|
||||
|
||||
## Features
|
||||
|
||||
- Interactive graph visualization
|
||||
- Multiple entity types (person, email, phone, address, domain, org, IP, social)
|
||||
- Auto-save to localStorage
|
||||
- Auto-save to IndexedDB
|
||||
- Import/Export JSON
|
||||
- Export PNG snapshots
|
||||
- Share link via base64 for smaller graphs
|
||||
- Undo/Redo support
|
||||
- Pan and zoom controls
|
||||
- PWA support (installable, offline-capable)
|
||||
- Self-hostable
|
||||
- Native desktop app support (via Wails)
|
||||
- Single-binary lightweight web server
|
||||
- Support for 32-bit and 64-bit architectures (runs on old Raspberry Pi Zero W)
|
||||
|
||||
## Installation Options
|
||||
|
||||
### Self-Hosted Web Server
|
||||
|
||||
The easiest way to self-host is using the single binary:
|
||||
|
||||
```sh
|
||||
task build
|
||||
./bin/linking-tool --port 8080
|
||||
```
|
||||
|
||||
The binary will be located in `bin/` after building.
|
||||
|
||||
### Desktop Application
|
||||
|
||||
Build the desktop application for your platform:
|
||||
|
||||
```sh
|
||||
task desktop-build
|
||||
```
|
||||
|
||||
The binary will be located in `bin/` after building.
|
||||
|
||||
## Development
|
||||
|
||||
### NPM
|
||||
### Prerequisites
|
||||
|
||||
- Go `1.25.5`
|
||||
- Node.js
|
||||
- pnpm
|
||||
- Wails (for desktop app development)
|
||||
|
||||
### Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev
|
||||
git clone https://git.quad4.io/quad4-software/linking-tool.git
|
||||
cd linking-tool
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Makefile
|
||||
### Task
|
||||
|
||||
```sh
|
||||
make dev
|
||||
The project uses [Task](https://taskfile.dev/) for all development tasks.
|
||||
|
||||
```
|
||||
| Task | Description |
|
||||
|---------------------|-------------------------------------------|
|
||||
| default | Show available tasks |
|
||||
| dev | Run development servers (Go & SvelteKit) |
|
||||
| build | Build the single binary web server |
|
||||
| build:frontend | Build frontend only |
|
||||
| build:backend | Build backend binary only |
|
||||
| package | Package the application |
|
||||
| release | Build binaries for all platforms |
|
||||
| build-linux-amd64 | Build Linux AMD64 binary |
|
||||
| build-linux-arm64 | Build Linux ARM64 binary |
|
||||
| build-linux-armv6 | Build Linux ARMv6 binary |
|
||||
| build-linux-armv7 | Build Linux ARMv7 binary |
|
||||
| build-windows-amd64 | Build Windows AMD64 binary |
|
||||
| build-darwin-amd64 | Build Darwin AMD64 binary |
|
||||
| build-darwin-arm64 | Build Darwin ARM64 binary |
|
||||
| build-freebsd-amd64 | Build FreeBSD AMD64 binary |
|
||||
| docker-build | Build Docker image |
|
||||
| docker-run | Run Docker container |
|
||||
| docker-builder | Build and extract binaries using Docker |
|
||||
| podman-build | Build Podman image |
|
||||
| podman-run | Run Podman container |
|
||||
| podman | Build and run Podman container |
|
||||
| desktop-build | Build desktop application |
|
||||
| desktop-linux | Build desktop application for Linux |
|
||||
| desktop-windows | Build desktop application for Windows |
|
||||
| desktop-darwin | Build desktop application for Darwin |
|
||||
| desktop-dev | Run desktop app in development mode |
|
||||
| clean | Clean build artifacts |
|
||||
| setup | Setup development environment |
|
||||
| install | Install dependencies |
|
||||
| install:ci | Install dependencies for CI (frozen lock) |
|
||||
| preview | Preview production build |
|
||||
| check | Run type checking |
|
||||
| lint | Run linter |
|
||||
| format | Format code |
|
||||
| version:minor | Bump minor version in package.json |
|
||||
| version:major | Bump major version in package.json |
|
||||
|
||||
example: task dev
|
||||
you might to set alias alias task=`go-task`
|
||||
```
|
||||
|
||||
## Docker
|
||||
## Contributing
|
||||
|
||||
Uses Chainguard Images which are rootless and very minimal images.
|
||||
Send us an email at [team@quad4.io](mailto:team@quad4.io) for any issues or feedback.
|
||||
|
||||
```sh
|
||||
docker build -t quad4-linking-tool .
|
||||
docker run -p 3000:3000 quad4-linking-tool
|
||||
```
|
||||
## License
|
||||
|
||||
## LICENSE
|
||||
|
||||
[MIT](LICENSE)
|
||||
[BSD 3-Clause](LICENSE)
|
||||
|
||||
17
SECURITY.md
Normal file
17
SECURITY.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Security Policy
|
||||
|
||||
If you have discovered a security vulnerability, please refer to [our website](https://quad4.io/security) for the latest security reporting procedures and guidelines.
|
||||
|
||||
## Vulnerability Management
|
||||
|
||||
- We use PNPM and [OSV](https://osv.dev/) to scan for package vulnerabilities in our dependencies.
|
||||
|
||||
## SAST
|
||||
|
||||
- Gosec for Go code.
|
||||
- ESLint with eslint-plugin-security for JavaScript code.
|
||||
|
||||
## Dependency and Supply Chain
|
||||
|
||||
- All GitHub Actions used are forked and hosted on our Gitea instance, view them here https://git.quad4.io/actions.
|
||||
- Actions are referenced using full URLs and cryptographically pinned to specific commit hashes for enhanced supply chain security.
|
||||
222
Taskfile.yml
Normal file
222
Taskfile.yml
Normal file
@@ -0,0 +1,222 @@
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
BINARY_NAME: linking-tool
|
||||
BUILD_DIR: bin
|
||||
|
||||
tasks:
|
||||
default:
|
||||
desc: Show available tasks
|
||||
cmds:
|
||||
- task --list
|
||||
|
||||
dev:
|
||||
desc: Run development servers (Go & SvelteKit)
|
||||
cmds:
|
||||
- pnpm install
|
||||
- pnpm run dev
|
||||
|
||||
build:frontend:
|
||||
desc: Build frontend only
|
||||
cmds:
|
||||
- pnpm run build
|
||||
|
||||
build:backend:
|
||||
desc: Build backend binary only
|
||||
cmds:
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} main.go
|
||||
|
||||
build:
|
||||
desc: Build the single binary web server
|
||||
cmds:
|
||||
- pnpm install
|
||||
- pnpm run build
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} main.go
|
||||
|
||||
package:
|
||||
desc: Package the application
|
||||
deps: [build]
|
||||
|
||||
release:
|
||||
desc: Build binaries for all platforms
|
||||
deps: [build]
|
||||
cmds:
|
||||
- task: build-linux-amd64
|
||||
- task: build-linux-arm64
|
||||
- task: build-linux-armv6
|
||||
- task: build-linux-armv7
|
||||
- task: build-windows-amd64
|
||||
- task: build-darwin-amd64
|
||||
- task: build-darwin-arm64
|
||||
- task: build-freebsd-amd64
|
||||
|
||||
build-linux-amd64:
|
||||
desc: Build Linux AMD64 binary
|
||||
cmds:
|
||||
- GOOS=linux GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-amd64 main.go
|
||||
|
||||
build-linux-arm64:
|
||||
desc: Build Linux ARM64 binary
|
||||
cmds:
|
||||
- GOOS=linux GOARCH=arm64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-arm64 main.go
|
||||
|
||||
build-linux-armv6:
|
||||
desc: Build Linux ARMv6 binary
|
||||
cmds:
|
||||
- GOOS=linux GOARCH=arm GOARM=6 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-armv6 main.go
|
||||
|
||||
build-linux-armv7:
|
||||
desc: Build Linux ARMv7 binary
|
||||
cmds:
|
||||
- GOOS=linux GOARCH=arm GOARM=7 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-linux-armv7 main.go
|
||||
|
||||
build-windows-amd64:
|
||||
desc: Build Windows AMD64 binary
|
||||
cmds:
|
||||
- GOOS=windows GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-windows-amd64.exe main.go
|
||||
|
||||
build-darwin-amd64:
|
||||
desc: Build Darwin AMD64 binary
|
||||
cmds:
|
||||
- GOOS=darwin GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-amd64 main.go
|
||||
|
||||
build-darwin-arm64:
|
||||
desc: Build Darwin ARM64 binary
|
||||
cmds:
|
||||
- GOOS=darwin GOARCH=arm64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-darwin-arm64 main.go
|
||||
|
||||
build-freebsd-amd64:
|
||||
desc: Build FreeBSD AMD64 binary
|
||||
cmds:
|
||||
- GOOS=freebsd GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 main.go
|
||||
|
||||
docker-build:
|
||||
desc: Build Docker image
|
||||
cmds:
|
||||
- docker build -f docker/Dockerfile -t {{.BINARY_NAME}} .
|
||||
|
||||
docker-run:
|
||||
desc: Run Docker container
|
||||
cmds:
|
||||
- docker run -p 8080:8080 {{.BINARY_NAME}}
|
||||
|
||||
docker-builder:
|
||||
desc: Build and extract binaries using Docker
|
||||
cmds:
|
||||
- docker build -f docker/Dockerfile.build -t {{.BINARY_NAME}}-build .
|
||||
- docker create --name {{.BINARY_NAME}}-temp {{.BINARY_NAME}}-build
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- docker cp {{.BINARY_NAME}}-temp:/bin/. {{.BUILD_DIR}}/
|
||||
- docker cp {{.BINARY_NAME}}-temp:/desktop-bin/. {{.BUILD_DIR}}/
|
||||
- docker rm {{.BINARY_NAME}}-temp
|
||||
|
||||
podman-build:
|
||||
desc: Build Podman image
|
||||
cmds:
|
||||
- podman build -f docker/Dockerfile -t surveilled .
|
||||
|
||||
podman-run:
|
||||
desc: Run Podman container
|
||||
cmds:
|
||||
- podman run --rm -p 3000:3000 surveilled
|
||||
|
||||
podman:
|
||||
desc: Build and run Podman container
|
||||
deps: [podman-build, podman-run]
|
||||
|
||||
desktop-build:
|
||||
desc: Build desktop application
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p desktop/frontend_dist
|
||||
- rm -rf desktop/frontend_dist/*
|
||||
- cp -r build/* desktop/frontend_dist/
|
||||
- cd desktop && wails build -s
|
||||
|
||||
desktop-linux:
|
||||
desc: Build desktop application for Linux
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p desktop/frontend_dist
|
||||
- rm -rf desktop/frontend_dist/*
|
||||
- cp -r build/* desktop/frontend_dist/
|
||||
- cd desktop && wails build -s -platform linux/amd64
|
||||
|
||||
desktop-windows:
|
||||
desc: Build desktop application for Windows
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p desktop/frontend_dist
|
||||
- rm -rf desktop/frontend_dist/*
|
||||
- cp -r build/* desktop/frontend_dist/
|
||||
- cd desktop && wails build -s -platform windows/amd64
|
||||
|
||||
desktop-darwin:
|
||||
desc: Build desktop application for Darwin
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p desktop/frontend_dist
|
||||
- rm -rf desktop/frontend_dist/*
|
||||
- cp -r build/* desktop/frontend_dist/
|
||||
- cd desktop && wails build -s -platform darwin/universal
|
||||
|
||||
desktop-dev:
|
||||
desc: Run desktop application in development mode
|
||||
deps: [build]
|
||||
cmds:
|
||||
- mkdir -p desktop/frontend_dist
|
||||
- rm -rf desktop/frontend_dist/*
|
||||
- cp -r build/* desktop/frontend_dist/
|
||||
- cd desktop && wails dev
|
||||
|
||||
clean:
|
||||
desc: Clean build artifacts
|
||||
cmds:
|
||||
- rm -rf .svelte-kit build node_modules node_modules/.vite dist package linking-tool tmp {{.BUILD_DIR}}
|
||||
|
||||
setup:
|
||||
desc: Setup development environment
|
||||
cmds:
|
||||
- corepack enable
|
||||
|
||||
install:
|
||||
desc: Install dependencies
|
||||
cmds:
|
||||
- pnpm install
|
||||
|
||||
install:ci:
|
||||
desc: Install dependencies for CI (frozen lockfile)
|
||||
cmds:
|
||||
- pnpm install --frozen-lockfile
|
||||
|
||||
preview:
|
||||
desc: Preview production build
|
||||
cmds:
|
||||
- pnpm run preview
|
||||
|
||||
check:
|
||||
desc: Run type checking
|
||||
cmds:
|
||||
- pnpm run check
|
||||
|
||||
lint:
|
||||
desc: Run linter
|
||||
cmds:
|
||||
- pnpm run lint
|
||||
|
||||
format:
|
||||
desc: Format code
|
||||
cmds:
|
||||
- pnpm run format
|
||||
|
||||
version:minor:
|
||||
desc: Bump minor version in package.json
|
||||
cmds:
|
||||
- pnpm version minor --no-git-tag-version
|
||||
|
||||
version:major:
|
||||
desc: Bump major version in package.json
|
||||
cmds:
|
||||
- pnpm version major --no-git-tag-version
|
||||
165
desktop/app.go
Normal file
165
desktop/app.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
port int
|
||||
debug bool
|
||||
}
|
||||
|
||||
// NewApp creates a new App struct
|
||||
func NewApp(debug bool) *App {
|
||||
return &App{
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) logDebug(format string, args ...any) {
|
||||
if a != nil && a.debug {
|
||||
fmt.Printf("[debug] "+format+"\n", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// logHandler wraps HTTP handlers to log requests when debug is enabled.
|
||||
func (a *App) logHandler(next http.Handler) http.Handler {
|
||||
if !a.debug {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
fmt.Printf("[debug] http %s %s %dms\n", r.Method, r.URL.Path, time.Since(start).Milliseconds())
|
||||
})
|
||||
}
|
||||
|
||||
// startup is called when the app starts. The context is saved
|
||||
// so we can call the runtime methods
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.logDebug("startup begin")
|
||||
|
||||
// Start local API server on a random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting local server: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.port = listener.Addr().(*net.TCPAddr).Port
|
||||
a.logDebug("local API listener bound on %s", listener.Addr().String())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// CORS middleware for local desktop API
|
||||
cors := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":"ok"}`)
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: listener.Addr().String(),
|
||||
Handler: a.logHandler(mux),
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Printf("Error serving desktop API: %v\n", err)
|
||||
}
|
||||
}()
|
||||
fmt.Printf("Desktop API server started on port %d\n", a.port)
|
||||
a.logDebug("startup complete")
|
||||
}
|
||||
|
||||
// GetAPIPort returns the port the local server is running on
|
||||
func (a *App) GetAPIPort() int {
|
||||
a.logDebug("GetAPIPort -> %d", a.port)
|
||||
return a.port
|
||||
}
|
||||
|
||||
// LogFrontend allows the frontend to log to the terminal
|
||||
func (a *App) LogFrontend(message string) {
|
||||
fmt.Printf("[frontend] %s\n", message)
|
||||
}
|
||||
|
||||
// SaveFile shows a save dialog and writes the content to the selected file
|
||||
func (a *App) SaveFile(filename string, content string) error {
|
||||
a.logDebug("SaveFile filename=%s", filename)
|
||||
filePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||
DefaultFilename: filename,
|
||||
Title: "Save Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if filePath == "" {
|
||||
return nil // Cancelled
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, []byte(content), 0600)
|
||||
}
|
||||
|
||||
// LoadFile shows an open dialog and returns the content of the selected file
|
||||
func (a *App) LoadFile() (string, error) {
|
||||
a.logDebug("LoadFile")
|
||||
filePath, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||
Title: "Open Graph",
|
||||
Filters: []runtime.FileFilter{
|
||||
{
|
||||
DisplayName: "JSON Files (*.json)",
|
||||
Pattern: "*.json",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if filePath == "" {
|
||||
return "", nil // Cancelled
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid file path: %w", err)
|
||||
}
|
||||
cleanPath := filepath.Clean(absPath)
|
||||
|
||||
content, err := os.ReadFile(cleanPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
52
desktop/main.go
Normal file
52
desktop/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend_dist
|
||||
var assets embed.FS
|
||||
|
||||
func debugEnabled() bool {
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg == "--debug" || arg == "-d" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func main() {
|
||||
debug := debugEnabled()
|
||||
if debug {
|
||||
println("Debug logging enabled")
|
||||
}
|
||||
|
||||
// Create an instance of the app structure
|
||||
app := NewApp(debug)
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Linking Tool",
|
||||
Width: 1280,
|
||||
Height: 800,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
EnableDefaultContextMenu: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
14
desktop/wails.json
Normal file
14
desktop/wails.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Linking Tool",
|
||||
"assetdir": "frontend_dist",
|
||||
"frontend:dir": "..",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm run build",
|
||||
"frontend:dev:watcher": "pnpm run dev",
|
||||
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||
"outputfilename": "linking-tool",
|
||||
"author": {
|
||||
"name": "Quad4",
|
||||
"email": "dev@quad4.io"
|
||||
}
|
||||
}
|
||||
35
docker/Dockerfile
Normal file
35
docker/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
||||
# Stage 1: Build the frontend
|
||||
FROM cgr.dev/chainguard/node:latest-dev AS node-builder
|
||||
WORKDIR /app
|
||||
USER root
|
||||
RUN corepack enable && corepack prepare pnpm@10.25.0 --activate
|
||||
USER node
|
||||
COPY --chown=node:node package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY --chown=node:node . .
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Build the Go binary with embedded assets
|
||||
FROM cgr.dev/chainguard/go:latest-dev AS go-builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
COPY --from=node-builder /app/build ./build
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o linking-tool main.go
|
||||
|
||||
# Stage 3: Minimal runtime image
|
||||
FROM cgr.dev/chainguard/wolfi-base:latest
|
||||
WORKDIR /app
|
||||
COPY --from=go-builder /app/linking-tool .
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
EXPOSE 8080
|
||||
ENV PORT=8080
|
||||
ENV HOST=0.0.0.0
|
||||
ENV NODE_ENV=production
|
||||
|
||||
USER 65532
|
||||
|
||||
CMD ["./linking-tool"]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import js from '@eslint/js';
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import securityPlugin from 'eslint-plugin-security';
|
||||
import sveltePlugin from 'eslint-plugin-svelte';
|
||||
import svelteParser from 'svelte-eslint-parser';
|
||||
|
||||
@@ -19,6 +20,8 @@ export default [
|
||||
caches: 'readonly',
|
||||
URL: 'readonly',
|
||||
console: 'readonly',
|
||||
Element: 'readonly',
|
||||
EventListener: 'readonly',
|
||||
HTMLElement: 'readonly',
|
||||
HTMLImageElement: 'readonly',
|
||||
HTMLInputElement: 'readonly',
|
||||
@@ -32,12 +35,28 @@ export default [
|
||||
Blob: 'readonly',
|
||||
Event: 'readonly',
|
||||
MouseEvent: 'readonly',
|
||||
TouchEvent: 'readonly',
|
||||
Touch: 'readonly',
|
||||
WheelEvent: 'readonly',
|
||||
KeyboardEvent: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
localStorage: 'readonly',
|
||||
sessionStorage: 'readonly',
|
||||
Response: 'readonly',
|
||||
Request: 'readonly',
|
||||
Headers: 'readonly',
|
||||
FormData: 'readonly',
|
||||
ServiceWorkerRegistration: 'readonly',
|
||||
location: 'readonly',
|
||||
history: 'readonly',
|
||||
addEventListener: 'readonly',
|
||||
removeEventListener: 'readonly',
|
||||
requestAnimationFrame: 'readonly',
|
||||
queueMicrotask: 'readonly',
|
||||
atob: 'readonly',
|
||||
btoa: 'readonly',
|
||||
alert: 'readonly',
|
||||
@@ -49,10 +68,12 @@ export default [
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
security: securityPlugin,
|
||||
svelte: sveltePlugin,
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...securityPlugin.configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -71,6 +92,36 @@ export default [
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['node_modules/**', '.svelte-kit/**', 'build/**', 'dist/**', 'archive/**'],
|
||||
files: ['bin/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['static/sw.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
self: 'readonly',
|
||||
caches: 'readonly',
|
||||
fetch: 'readonly',
|
||||
URL: 'readonly',
|
||||
console: 'readonly',
|
||||
Response: 'readonly',
|
||||
Request: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'.svelte-kit/**',
|
||||
'build/**',
|
||||
'dist/**',
|
||||
'archive/**',
|
||||
'desktop/frontend_dist/**',
|
||||
'wailsjs/**',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766902085,
|
||||
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
94
flake.nix
Normal file
94
flake.nix
Normal file
@@ -0,0 +1,94 @@
|
||||
{
|
||||
description = "Quad4 Linking Tool development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
go = pkgs.go_1_25;
|
||||
|
||||
task = pkgs.buildGoModule rec {
|
||||
pname = "task";
|
||||
version = "3.46.3";
|
||||
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "go-task";
|
||||
repo = "task";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-1bS8ZZAcemgRG7PTeGTFfd49T9u6U6CxxrbotwCM15A=";
|
||||
};
|
||||
|
||||
vendorHash = "sha256-Tm0tqureCRwcP5KKDTa9TO1yZ3Px3ulf9/jKQDDTjDw=";
|
||||
subPackages = [ "cmd/task" ];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "A task runner / simpler Make alternative written in Go";
|
||||
homepage = "https://taskfile.dev/";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
};
|
||||
};
|
||||
|
||||
wailsSrc = pkgs.fetchFromGitHub {
|
||||
owner = "wailsapp";
|
||||
repo = "wails";
|
||||
rev = "v2.11.0";
|
||||
hash = "sha256-H1Nml2vhCx4IB/CT+kDro5joAw8ewpxoQjDgvqamAr8=";
|
||||
};
|
||||
|
||||
wails = pkgs.buildGoModule rec {
|
||||
pname = "wails";
|
||||
version = "2.11.0";
|
||||
|
||||
src = pkgs.runCommand "${pname}-${version}-src" {} ''
|
||||
cp -r ${wailsSrc}/v2 $out
|
||||
chmod -R +w $out
|
||||
'';
|
||||
|
||||
vendorHash = "sha256-HAIKhMKRTNI4hsm8Hvn5pUhnCTcitRxiw+WkVmxpfiU=";
|
||||
subPackages = [ "cmd/wails" ];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Build applications using Go + HTML + CSS + JS";
|
||||
homepage = "https://wails.io/";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
task
|
||||
nodejs_20
|
||||
nodePackages.pnpm
|
||||
wails
|
||||
gcc
|
||||
pkg-config
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "Quad4 Linking Tool Development Environment"
|
||||
echo "Go version: $(go version)"
|
||||
echo "Task version: $(task --version 2>/dev/null || echo 'installed')"
|
||||
echo "Node version: $(node --version)"
|
||||
echo "pnpm version: $(pnpm --version)"
|
||||
echo "Wails version: $(wails version 2>/dev/null || echo 'installed')"
|
||||
'';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
35
go.mod
Normal file
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module git.quad4.io/Quad4-Software/linking-tool
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/wailsapp/wails/v2 v2.11.0
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
81
go.sum
Normal file
81
go.sum
Normal file
@@ -0,0 +1,81 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
146
main.go
Normal file
146
main.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed build/*
|
||||
var buildAssets embed.FS
|
||||
|
||||
func corsMiddleware(allowedOrigins []string) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
allowed := false
|
||||
if len(allowedOrigins) == 0 {
|
||||
allowed = true
|
||||
} else {
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" || o == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
}
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
if allowed {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed && len(allowedOrigins) > 0 {
|
||||
log.Printf("Blocked CORS request from origin: %s", origin)
|
||||
http.Error(w, "CORS Origin Not Allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
frontendPath := flag.String("frontend", "", "Path to custom frontend build directory (overrides embedded assets)")
|
||||
host := flag.String("host", "127.0.0.1", "Host to bind the server to")
|
||||
port := flag.String("port", "", "Port to listen on (overrides PORT env var)")
|
||||
allowedOriginsStr := flag.String("allowed-origins", os.Getenv("ALLOWED_ORIGINS"), "Comma-separated list of allowed CORS origins")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var allowedOrigins []string
|
||||
if *allowedOriginsStr != "" {
|
||||
origins := strings.Split(*allowedOriginsStr, ",")
|
||||
for _, o := range origins {
|
||||
allowedOrigins = append(allowedOrigins, strings.TrimSpace(o))
|
||||
}
|
||||
}
|
||||
|
||||
if hostEnv := os.Getenv("HOST"); hostEnv != "" {
|
||||
*host = hostEnv
|
||||
}
|
||||
|
||||
if *port == "" {
|
||||
*port = os.Getenv("PORT")
|
||||
if *port == "" {
|
||||
*port = "8080"
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware chains
|
||||
cors := corsMiddleware(allowedOrigins)
|
||||
|
||||
http.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := w.Write([]byte(`{"status":"ok"}`)); err != nil {
|
||||
log.Printf("Error writing response: %v", err)
|
||||
}
|
||||
}))
|
||||
|
||||
// Static Assets
|
||||
var staticFS fs.FS
|
||||
if *frontendPath != "" {
|
||||
log.Printf("Using custom frontend from: %s\n", *frontendPath)
|
||||
staticFS = os.DirFS(*frontendPath)
|
||||
} else {
|
||||
sub, err := fs.Sub(buildAssets, "build")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
staticFS = sub
|
||||
}
|
||||
|
||||
fileServer := http.FileServer(http.FS(staticFS))
|
||||
|
||||
// SPA Handler
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
_, err := staticFS.Open(path)
|
||||
if err != nil {
|
||||
// If file doesn't exist, serve index.html for SPA routing
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
addr := net.JoinHostPort(*host, *port)
|
||||
log.Printf("Linking Tool server starting on %s...\n", addr)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: nil,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
4125
package-lock.json
generated
4125
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -1,34 +1,61 @@
|
||||
{
|
||||
"name": "quad4-linking-tool",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"name": "@quad4/linking-tool",
|
||||
"version": "1.5.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"author": "Quad4",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
"bin": {
|
||||
"linking-tool": "./bin/linking-tool.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"publishConfig": {
|
||||
"registry": "https://git.quad4.io/api/packages/quad4-software/npm/"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"cookie": "1.1.1"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"build/**/*",
|
||||
"bin/**/*",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
|
||||
"prebuild": "node scripts/inject-sw-version.js",
|
||||
"build": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
|
||||
"preview": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"package": "svelte-kit sync && VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
|
||||
"desktop:dev": "task desktop-dev",
|
||||
"desktop:build": "task desktop-build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/kit": "^2.49.1",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.49.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
||||
"@typescript-eslint/parser": "^8.51.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"eslint-plugin-svelte": "^3.13.1",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"svelte": "^5.45.6",
|
||||
"svelte-check": "^4.3.4",
|
||||
"svelte": "^5.46.1",
|
||||
"svelte-check": "^4.3.5",
|
||||
"svelte-eslint-parser": "^1.4.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.2.6"
|
||||
"vite": "^7.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.23",
|
||||
|
||||
2549
pnpm-lock.yaml
generated
Normal file
2549
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
||||
8856
sbom/sbom.cyclonedx.json
Normal file
8856
sbom/sbom.cyclonedx.json
Normal file
File diff suppressed because it is too large
Load Diff
12578
sbom/sbom.spdx.json
Normal file
12578
sbom/sbom.spdx.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,5 +2,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
echo "Building app..."
|
||||
VITE_APP_VERSION=$(node -p "require('./package.json').version") npm run build
|
||||
VITE_APP_VERSION=$(node -p "require('./package.json').version") pnpm run build
|
||||
|
||||
|
||||
23
scripts/inject-sw-version.js
Normal file
23
scripts/inject-sw-version.js
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
||||
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
|
||||
const version = packageJson.version;
|
||||
|
||||
const swPath = join(rootDir, 'static', 'sw.js');
|
||||
let swContent = readFileSync(swPath, 'utf-8');
|
||||
|
||||
swContent = swContent.replace(
|
||||
/const CACHE_VERSION = ['"](.*?)['"];/,
|
||||
`const CACHE_VERSION = '${version}';`
|
||||
);
|
||||
|
||||
writeFileSync(swPath, swContent);
|
||||
console.log(`Injected version ${version} into service worker`);
|
||||
@@ -23,20 +23,16 @@ VULNS=$(jq -r '
|
||||
.results[]? |
|
||||
.source as $src |
|
||||
.vulns[]? |
|
||||
select(
|
||||
(.database_specific.severity // "" | ascii_upcase | test("HIGH|CRITICAL")) or
|
||||
(.severity[]?.score // "" | tostring | split("/")[0] | tonumber? // 0 | . >= 7.0)
|
||||
) |
|
||||
"\(.id) (source: \($src))"
|
||||
' "$OSV_JSON")
|
||||
|
||||
if [ -n "$VULNS" ]; then
|
||||
echo "OSV scan found HIGH/CRITICAL vulnerabilities:"
|
||||
echo "OSV scan found vulnerabilities:"
|
||||
echo "$VULNS" | while IFS= read -r line; do
|
||||
echo " - $line"
|
||||
done
|
||||
exit 1
|
||||
else
|
||||
echo "OSV scan: no HIGH/CRITICAL vulnerabilities found."
|
||||
echo "OSV scan: no vulnerabilities found."
|
||||
fi
|
||||
|
||||
|
||||
BIN
showcase/linkingtool.png
Normal file
BIN
showcase/linkingtool.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
13
src/app.d.ts
vendored
13
src/app.d.ts
vendored
@@ -8,6 +8,19 @@ declare global {
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
go?: {
|
||||
main: {
|
||||
App: {
|
||||
SaveFile(filename: string, content: string): Promise<string>;
|
||||
LoadFile(): Promise<string>;
|
||||
LogFrontend(message: string): void;
|
||||
};
|
||||
};
|
||||
};
|
||||
runtime?: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
80
src/lib/constants.ts
Normal file
80
src/lib/constants.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { User, Mail, Phone, MapPin, Globe, Building2, Network, AtSign } from 'lucide-svelte';
|
||||
|
||||
export const DB_NAME = 'quad4-linking-db';
|
||||
export const DB_VERSION = 2;
|
||||
export const STORE_NAME = 'graphs';
|
||||
export const SETTINGS_STORE = 'settings';
|
||||
export const UNDO_STORE = 'undo_stack';
|
||||
export const REDO_STORE = 'redo_stack';
|
||||
|
||||
export const MAX_HISTORY = 100;
|
||||
|
||||
export const ALLOWED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
export const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
export type RelationshipType =
|
||||
| 'Linked'
|
||||
| 'Works For'
|
||||
| 'Knows'
|
||||
| 'Owns'
|
||||
| 'Associated With'
|
||||
| 'Related To'
|
||||
| 'Connected To';
|
||||
|
||||
export type RelationshipStrength = 'weak' | 'medium' | 'strong';
|
||||
|
||||
export const RELATIONSHIP_COLORS: Record<RelationshipType, string> = {
|
||||
Linked: '#525252',
|
||||
'Works For': '#3b82f6',
|
||||
Knows: '#10b981',
|
||||
Owns: '#f59e0b',
|
||||
'Associated With': '#8b5cf6',
|
||||
'Related To': '#ec4899',
|
||||
'Connected To': '#06b6d4',
|
||||
};
|
||||
|
||||
export const relationshipTypes: RelationshipType[] = [
|
||||
'Linked',
|
||||
'Works For',
|
||||
'Knows',
|
||||
'Owns',
|
||||
'Associated With',
|
||||
'Related To',
|
||||
'Connected To',
|
||||
];
|
||||
|
||||
export const relationshipStrengths: RelationshipStrength[] = ['weak', 'medium', 'strong'];
|
||||
|
||||
export type NodeType =
|
||||
| 'person'
|
||||
| 'email'
|
||||
| 'phone'
|
||||
| 'address'
|
||||
| 'domain'
|
||||
| 'org'
|
||||
| 'ip'
|
||||
| 'social';
|
||||
|
||||
export const iconMap: Record<NodeType, typeof User> = {
|
||||
person: User,
|
||||
email: Mail,
|
||||
phone: Phone,
|
||||
address: MapPin,
|
||||
domain: Globe,
|
||||
org: Building2,
|
||||
ip: Network,
|
||||
social: AtSign,
|
||||
};
|
||||
|
||||
export const nodeTypes = Object.keys(iconMap) as NodeType[];
|
||||
|
||||
export const typeColors: Record<NodeType, string> = {
|
||||
person: '#ef4444',
|
||||
email: '#f97316',
|
||||
phone: '#eab308',
|
||||
address: '#10b981',
|
||||
domain: '#f43f5e',
|
||||
org: '#be123c',
|
||||
ip: '#71717a',
|
||||
social: '#db2777',
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { User, Mail, Phone, MapPin, Globe, Building2, Network, AtSign } from 'lucide-svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export type NodeType =
|
||||
| 'person'
|
||||
| 'email'
|
||||
| 'phone'
|
||||
| 'address'
|
||||
| 'domain'
|
||||
| 'org'
|
||||
| 'ip'
|
||||
| 'social';
|
||||
|
||||
export const iconMap: Record<NodeType, ComponentType> = {
|
||||
person: User,
|
||||
email: Mail,
|
||||
phone: Phone,
|
||||
address: MapPin,
|
||||
domain: Globe,
|
||||
org: Building2,
|
||||
ip: Network,
|
||||
social: AtSign,
|
||||
};
|
||||
|
||||
export const nodeTypes = Object.keys(iconMap) as NodeType[];
|
||||
|
||||
export const typeColors: Record<NodeType, string> = {
|
||||
person: '#ef4444',
|
||||
email: '#f97316',
|
||||
phone: '#eab308',
|
||||
address: '#10b981',
|
||||
domain: '#f43f5e',
|
||||
org: '#be123c',
|
||||
ip: '#71717a',
|
||||
social: '#db2777',
|
||||
};
|
||||
@@ -1 +1,20 @@
|
||||
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'dev';
|
||||
declare const __APP_VERSION__: string | undefined;
|
||||
|
||||
type ProcessLike = {
|
||||
env?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
const definedVersion =
|
||||
typeof __APP_VERSION__ !== 'undefined' && __APP_VERSION__ ? __APP_VERSION__ : undefined;
|
||||
|
||||
const processEnv =
|
||||
typeof globalThis === 'object' && globalThis !== null
|
||||
? ((globalThis as unknown as { process?: ProcessLike }).process?.env ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const envVersion =
|
||||
typeof processEnv?.npm_package_version === 'string' && processEnv.npm_package_version
|
||||
? processEnv.npm_package_version
|
||||
: undefined;
|
||||
|
||||
export const APP_VERSION = definedVersion ?? envVersion ?? 'dev';
|
||||
|
||||
@@ -1,13 +1,64 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { onMount } from 'svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
let showUpdateAvailable = $state(false);
|
||||
let registration: ServiceWorkerRegistration | null = null;
|
||||
|
||||
function checkForUpdates() {
|
||||
if (registration && navigator.onLine) {
|
||||
registration.update().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
function reloadApp() {
|
||||
if (registration && registration.waiting) {
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then((registration) => {
|
||||
console.log('Service Worker registered:', registration);
|
||||
.then((reg) => {
|
||||
registration = reg;
|
||||
|
||||
reg.addEventListener('updatefound', () => {
|
||||
const newWorker = reg.installing;
|
||||
if (newWorker) {
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed') {
|
||||
if (reg.waiting) {
|
||||
showUpdateAvailable = true;
|
||||
} else if (navigator.serviceWorker.controller) {
|
||||
showUpdateAvailable = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (reg.waiting) {
|
||||
showUpdateAvailable = true;
|
||||
}
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
if (navigator.onLine) {
|
||||
setInterval(() => {
|
||||
checkForUpdates();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
checkForUpdates();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
@@ -16,4 +67,43 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
{#if showUpdateAvailable}
|
||||
<div
|
||||
class="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 bg-neutral-900 border border-neutral-800 rounded-lg shadow-lg p-4 max-w-md mx-4"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-white">Update Available</p>
|
||||
<p class="text-xs text-neutral-400 mt-1">A new version is available. Reload to update.</p>
|
||||
</div>
|
||||
<button
|
||||
onclick={reloadApp}
|
||||
class="px-4 py-2 bg-accent-red text-white rounded-md text-sm font-medium hover:bg-accent-red-dark transition-colors"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showUpdateAvailable = false)}
|
||||
class="text-neutral-400 hover:text-white transition-colors"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{@render children()}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<title>Linking Tool - Identity Graph</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Linking Tool - A client-side identity graph visualization tool for mapping relationships between entities."
|
||||
content="Linking Tool - A client-side web linking tool for mapping relationships between entities."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Linking Tool - Identity Graph" />
|
||||
<meta property="og:title" content="Linking Tool" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A client-side identity graph visualization tool for mapping relationships between entities."
|
||||
content="A client-side web linking tool for mapping relationships between entities."
|
||||
/>
|
||||
<meta property="og:image" content="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
@@ -22,17 +22,22 @@
|
||||
|
||||
<div class="flex flex-col h-screen bg-bg-primary text-text-primary">
|
||||
<header
|
||||
class="bg-neutral-950 border-b border-neutral-800 px-4 sm:px-6 py-3 flex flex-col sm:flex-row justify-between items-center gap-2 flex-shrink-0 shadow-lg"
|
||||
class="bg-neutral-950 border-b border-neutral-800 px-2 sm:px-6 py-1.5 sm:py-3 flex flex-col sm:flex-row justify-between items-center gap-1 sm:gap-2 flex-shrink-0 shadow-lg"
|
||||
>
|
||||
<h1 class="text-lg sm:text-xl font-semibold text-accent-red-light flex items-center gap-2">
|
||||
<a
|
||||
href="https://git.quad4.io/Quad4-Software/Linking-Tool"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm sm:text-xl font-semibold text-accent-red-light flex items-center gap-1.5 sm:gap-2 hover:text-accent-red-dark transition-colors"
|
||||
>
|
||||
<div
|
||||
class="h-5 w-5 rounded border border-accent-red-light flex items-center justify-center bg-neutral-900"
|
||||
class="h-4 w-4 sm:h-5 sm:w-5 rounded border border-accent-red-light flex items-center justify-center bg-neutral-900"
|
||||
>
|
||||
<LinkIcon size={14} class="text-accent-red-light" />
|
||||
<LinkIcon size={12} class="sm:w-[14px] sm:h-[14px] text-accent-red-light" />
|
||||
</div>
|
||||
Linking Tool
|
||||
</h1>
|
||||
<div class="text-text-secondary text-xs sm:text-sm flex items-center gap-2">
|
||||
</a>
|
||||
<div class="text-text-secondary text-[10px] sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<span
|
||||
>Created by <a
|
||||
href="https://quad4.io"
|
||||
@@ -51,7 +56,7 @@
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex-1 relative overflow-hidden bg-bg-primary p-4">
|
||||
<main class="flex-1 relative overflow-hidden bg-bg-primary p-0 sm:p-4">
|
||||
<IdentityGraph />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
36
static/sw.js
36
static/sw.js
@@ -1,5 +1,5 @@
|
||||
/* eslint-env serviceworker */
|
||||
const CACHE_NAME = 'quad4-linking-tool-v1';
|
||||
const CACHE_VERSION = '1.5.1';
|
||||
const CACHE_NAME = `quad4-linking-tool-${CACHE_VERSION}`;
|
||||
const urlsToCache = ['/', '/favicon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
@@ -18,7 +18,7 @@ self.addEventListener('activate', (event) => {
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
if (cacheName !== CACHE_NAME && cacheName.startsWith('quad4-linking-tool-')) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
@@ -28,26 +28,40 @@ self.addEventListener('activate', (event) => {
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.request.url.startsWith(self.location.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((response) => {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
return fetch(event.request).then((response) => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return fetch(event.request)
|
||||
.then((response) => {
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('/') || new Response('Offline', { status: 503 });
|
||||
});
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,17 +1,28 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
compilerOptions: (id) => {
|
||||
if (id && id.includes('node_modules')) {
|
||||
return {
|
||||
runes: false, // Disable runes for external dependencies that might not support it yet (lucide-svelte)
|
||||
};
|
||||
}
|
||||
return {
|
||||
runes: true,
|
||||
};
|
||||
},
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter(),
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ export default {
|
||||
fontFamily: {
|
||||
sans: ['Nunito', 'Inter', 'Segoe UI', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
},
|
||||
screens: {
|
||||
'mobile-landscape': { raw: '(max-width: 767px) and (orientation: landscape)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import pkg from './package.json' with { type: 'json' };
|
||||
|
||||
declare const process: {
|
||||
env: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
const appVersion = process.env.VITE_APP_VERSION ?? pkg.version ?? 'dev';
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(appVersion),
|
||||
},
|
||||
plugins: [sveltekit()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user