6 Commits

Author SHA1 Message Date
8b5c52dd1e fixing pipeline
All checks were successful
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 9s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Successful in 55s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Successful in 1s
2026-07-10 22:53:05 -03:00
c3addb6d02 adding overwrite flag
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 9s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 47s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:46:53 -03:00
5597afc572 fixing job
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 8s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 2s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:44:57 -03:00
30372f800c adding pr support
Some checks failed
Build Pull Request / Build Pull Request Archive (pull_request) Successful in 42s
Build Pull Request / Capture Pull Request Screenshots (pull_request) Failing after 6s
Build Pull Request / Publish Pull Request Screenshot Preview (pull_request) Has been skipped
2026-07-10 22:41:00 -03:00
9e738cc0d5 Setting version from tag 2026-07-10 22:08:23 -03:00
b60c88e9b1 adding license and privacy policy
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 8s
Build and Release to Staging / Build Vision Start Image (push) Successful in 1m2s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 1s
2026-07-10 22:07:44 -03:00
10 changed files with 596 additions and 7 deletions

View File

@@ -0,0 +1,175 @@
name: Build Pull Request
on:
pull_request:
types:
- opened
- reopened
- synchronize
env:
ARCHIVE_NAME: vision-start-pr-${{ gitea.event.pull_request.number }}.zip
jobs:
build:
name: Build Pull Request Archive
runs-on: ubuntu-amd64
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up required tools
run: sudo apt-get install zip wget -y
- name: Install JS dependencies
run: npm ci
- name: Run build
run: |
bash scripts/prepare_release.sh
npm run build
- name: Prepare archive
run: |
mkdir vision-start
mv dist vision-start/
mv extension vision-start/
mv manifest.json vision-start/
- name: Create zip archive
run: |
cd vision-start
zip -r ../${{ env.ARCHIVE_NAME }} *
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: pr-release-zip
path: ${{ env.ARCHIVE_NAME }}
capture_screenshots:
name: Capture Pull Request Screenshots
runs-on: ubuntu-amd64
needs: build
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Set up required tools
run: sudo apt-get install unzip -y
- name: Install JS dependencies
run: npm ci
- name: Download build artifact
uses: actions/download-artifact@v3
with:
name: pr-release-zip
- name: Unpack build artifact
run: |
unzip -o -q ${{ env.ARCHIVE_NAME }}
if [ ! -d dist ]; then
echo "The build artifact does not contain dist/."
unzip -l ${{ env.ARCHIVE_NAME }}
exit 1
fi
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Capture screenshots
run: npm run capture:screenshots
- name: Upload screenshot artifact
uses: actions/upload-artifact@v3
with:
name: pr-screenshots
path: |
screenshots/home.png
screenshots/editing.png
screenshots/configuration.png
publish_screenshot_preview:
name: Publish Pull Request Screenshot Preview
runs-on: ubuntu-amd64
needs: capture_screenshots
if: ${{ gitea.event.pull_request.head.repo.full_name == gitea.repository }}
permissions:
actions: read
issues: write
steps:
- name: Set up required tools
run: sudo apt-get install curl jq -y
- name: Download screenshot artifact
uses: actions/download-artifact@v3
with:
name: pr-screenshots
path: screenshots
- name: Update pull request screenshot preview
continue-on-error: true
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_TOKEN: ${{ gitea.token }}
REPOSITORY: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }}
run: |
set -euo pipefail
OWNER="${REPOSITORY%%/*}"
REPO="${REPOSITORY#*/}"
MARKER='<!-- vision-start-pr-screenshots -->'
COMMENTS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/$PR_NUMBER/comments"
COMMENTS=$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$COMMENTS_URL")
COMMENT_ID=$(printf '%s' "$COMMENTS" | jq -r --arg marker "$MARKER" '.[] | select(.body | contains($marker)) | .id' | tail -n 1)
if [ -z "$COMMENT_ID" ] || [ "$COMMENT_ID" = "null" ]; then
PAYLOAD=$(jq -n --arg body "$MARKER" '{body: $body}')
COMMENT_ID=$(curl -fsS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
--data "$PAYLOAD" \
"$COMMENTS_URL" | jq -r '.id')
fi
ASSETS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID/assets"
for ASSET_ID in $(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL" | jq -r '.[].id'); do
curl -fsS -X DELETE -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL/$ASSET_ID"
done
upload_asset() {
curl -fsS -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$1;type=image/png" \
"$ASSETS_URL" | jq -r '.browser_download_url'
}
HOME_URL=$(upload_asset screenshots/home.png)
EDITING_URL=$(upload_asset screenshots/editing.png)
CONFIGURATION_URL=$(upload_asset screenshots/configuration.png)
BODY=$(printf '%s\n\n%s\n\n%s\n%s\n\n%s\n%s\n\n%s\n%s' \
"$MARKER" \
'## Visual preview' \
'### Home' \
"![Home]($HOME_URL)" \
'### Editing' \
"![Editing]($EDITING_URL)" \
'### Configuration' \
"![Configuration]($CONFIGURATION_URL)")
PAYLOAD=$(jq -n --arg body "$BODY" '{body: $body}')
curl -fsS -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H 'Content-Type: application/json' \
--data "$PAYLOAD" \
"$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID"

View File

@@ -20,6 +20,25 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set version from tag
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Expected a vX.Y.Z tag, got: $RELEASE_TAG"
exit 1
fi
VERSION="${RELEASE_TAG#v}"
MATCHING_FILES=$(git grep -Il -e '\bv0\.0\.0\b' -e '\b0\.0\.0\b' || true)
if [ -n "$MATCHING_FILES" ]; then
while IFS= read -r file; do
sed -i \
-e "s/\\bv0\\.0\\.0\\b/$RELEASE_TAG/g" \
-e "s/\\b0\\.0\\.0\\b/$VERSION/g" \
"$file"
done <<< "$MATCHING_FILES"
fi
- name: Setup required tools - name: Setup required tools
run: sudo apt-get install zip jq curl -y run: sudo apt-get install zip jq curl -y
@@ -83,7 +102,7 @@ jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build, virus-total-check] needs: [build, virus-total-check, capture_screenshots]
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -91,6 +110,11 @@ jobs:
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: release-zip name: release-zip
- name: Download screenshot artifacts
uses: actions/download-artifact@v3
with:
name: release-screenshots
path: release-screenshots
- name: Release zip - name: Release zip
uses: akkuman/gitea-release-action@v1 uses: akkuman/gitea-release-action@v1
with: with:
@@ -101,7 +125,11 @@ jobs:
**Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }} **Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }}
name: ${{ gitea.ref_name }} name: ${{ gitea.ref_name }}
tag_name: ${{ gitea.ref_name }} tag_name: ${{ gitea.ref_name }}
files: vision-start-${{ gitea.ref_name }}.zip files: |
vision-start-${{ gitea.ref_name }}.zip
release-screenshots/home.png
release-screenshots/editing.png
release-screenshots/configuration.png
build_vision_start: build_vision_start:
name: Build Vision Start Image name: Build Vision Start Image
@@ -111,6 +139,25 @@ jobs:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set version from tag
env:
RELEASE_TAG: ${{ gitea.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Expected a vX.Y.Z tag, got: $RELEASE_TAG"
exit 1
fi
VERSION="${RELEASE_TAG#v}"
MATCHING_FILES=$(git grep -Il -e '\bv0\.0\.0\b' -e '\b0\.0\.0\b' || true)
if [ -n "$MATCHING_FILES" ]; then
while IFS= read -r file; do
sed -i \
-e "s/\\bv0\\.0\\.0\\b/$RELEASE_TAG/g" \
-e "s/\\b0\\.0\\.0\\b/$VERSION/g" \
"$file"
done <<< "$MATCHING_FILES"
fi
- name: Log in to Container Registry - name: Log in to Container Registry
run: | run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" \ echo "${{ secrets.REGISTRY_PASSWORD }}" \
@@ -146,3 +193,36 @@ jobs:
cd ${{ secrets.PROD_DIR }} cd ${{ secrets.PROD_DIR }}
docker compose pull docker compose pull
docker compose up -d --force-recreate docker compose up -d --force-recreate
capture_screenshots:
name: Capture Vision Start Screenshots
runs-on: ubuntu-amd64
needs: deploy_vision_start
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install JS dependencies
run: npm ci
- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium
- name: Capture release screenshots
env:
SCREENSHOT_BASE_URL: http://vision-start.ivanch.me
run: npm run capture:screenshots
- name: Upload release screenshots
uses: actions/upload-artifact@v3
with:
name: release-screenshots
path: |
screenshots/home.png
screenshots/editing.png
screenshots/configuration.png

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Vision Startpage Project
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

60
PRIVACY.md Normal file
View File

@@ -0,0 +1,60 @@
# Privacy Policy for Vision Start
This Privacy Policy describes how the Vision Start Chrome extension handles user information.
## Information Stored by the Extension
Vision Start stores extension settings and user-provided configuration locally in the browser using Chrome's local extension storage.
This information may include:
* Start-page preferences and appearance settings;
* Bookmarks, shortcuts, or server and services addresses added by the user;
* Configuration for features enabled by the user.
This information is used only to provide the extension's functionality. It is not transmitted to, collected by, or stored on servers operated by the developer.
The developer does not have access to information stored locally by the extension.
## Server Status Requests
Vision Start can send network requests to server addresses configured by the user in order to check whether those servers are available.
These requests are sent directly from the user's browser to the configured server. The developer does not receive, proxy, log, or store these requests.
The destination server may receive standard network information associated with the request, such as the user's IP address, browser request headers, and the requested server address. The handling of that information is controlled by the operator of the destination server.
## External Favicon Service
Vision Start uses the Google Favicon Service (https://www.google.com/s2/favicons) to retrieve icons for websites configured by the user.
When an icon is requested, the relevant website domain is sent directly from the user's browser to Google. Google may also receive standard information associated with the network request, such as the user's IP address, browser request headers, and the requested domain.
The developer does not receive, proxy, log, or store these requests. Google's handling of information is governed by the Google Privacy Policy.
## Data Sharing and Sale
The developer does not sell, rent, trade, or use user information for advertising, analytics, profiling, or marketing.
No locally stored extension data is shared with the developer or with third parties, except for the direct network requests described above that are necessary to provide user-requested features.
## Data Retention and Deletion
Information stored by Vision Start remains in the browser until the user:
* Deletes the relevant information through the extension;
* Clears the extension's browser storage; or
* Uninstalls the extension.
## Security
Vision Start is designed to keep user configuration on the user's device. Users should use HTTPS addresses whenever possible when configuring remote servers or external resources.
## Changes to This Policy
This Privacy Policy may be updated if the extension's functionality or data-handling practices change.
Any updated version will be published at this location with a revised effective date. Material changes to how user information is handled will also be disclosed through the extension or its Chrome Web Store listing where appropriate.
## Contact
Questions about this Privacy Policy may be submitted through the developer's GitHub profile: https://github.com/ivanch

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Vision Startpage", "name": "Vision Startpage",
"version": "1.0.0", "version": "0.0.0",
"description": "A light, modern and customizable startpage.", "description": "A light, modern and customizable startpage.",
"chrome_url_overrides": { "chrome_url_overrides": {
"newtab": "index.html" "newtab": "index.html"

48
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"playwright": "1.61.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.7.2", "typescript": "~5.7.2",
@@ -2387,6 +2388,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",

View File

@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"capture:screenshots": "node scripts/capture_screenshots.mjs"
}, },
"dependencies": { "dependencies": {
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
@@ -20,6 +21,7 @@
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.5",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"playwright": "1.61.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"typescript": "~5.7.2", "typescript": "~5.7.2",

View File

@@ -40,6 +40,7 @@ Live instances / artifacts:
| Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` | | Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` |
| Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` | | Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` |
| CI/CD | Gitea Actions workflows (`.gitea/workflows/`) | | CI/CD | Gitea Actions workflows (`.gitea/workflows/`) |
| Release screenshots | Playwright 1.61.1 + Chromium capture the deployed production page at a fixed 1280×800 viewport |
Entry points: `index.html``index.tsx``App.tsx`. Entry points: `index.html``index.tsx``App.tsx`.
@@ -125,13 +126,16 @@ vision-start/
│ ├── favicon.ico │ ├── favicon.ico
│ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build) │ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build)
├── screenshots/ # README screenshots (dark page, editing, configuration) ├── screenshots/ # README/release screenshots (home, editing, configuration; regenerated at 1280×800)
├── scripts/ ├── scripts/
│ ├── capture_screenshots.mjs # Loads demoData, captures home/edit/configuration with Playwright, and can target a deployed URL
│ ├── demoData.json # Base64-encoded localStorage fixture used for release screenshots
│ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons │ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons
│ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict │ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict
├── .gitea/workflows/ ├── .gitea/workflows/
│ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging │ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging
│ ├── pull-request.yaml # On PR updates: build/zip, capture screenshots, and publish an inline visual preview
│ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod │ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod
├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf) ├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf)
@@ -208,6 +212,9 @@ Build, then combine `dist/` + `manifest.json` into a folder and "Load unpacked"
`Dockerfile` builds in Node 22 Alpine (`npm ci` → runs `scripts/prepare_release.sh``npm run build`) and serves `/app/dist` + `manifest.json` via nginx:alpine on port 80. `Dockerfile` builds in Node 22 Alpine (`npm ci` → runs `scripts/prepare_release.sh``npm run build`) and serves `/app/dist` + `manifest.json` via nginx:alpine on port 80.
### CI/CD (Gitea Actions) ### CI/CD (Gitea Actions)
- `release.yaml` validates each `vX.Y.Z` tag and replaces tracked `v0.0.0`/`0.0.0` placeholders in each build checkout before producing the extension archive and production image.
- **`pull-request.yaml`** — Triggers on pull request open, reopen, and synchronization. It builds and uploads a PR extension archive containing `dist/`, unpacks that archive in a separate Playwright job to generate the three demo screenshots, and uploads both artifacts. For same-repository PRs, it maintains one Gitea PR comment with inline image attachments; fork PRs retain artifacts but skip the comment because their workflow token is read-only.
- After `deploy_vision_start` succeeds, `release.yaml` uses Playwright Chromium against the deployed production page and seeds each browser context from `scripts/demoData.json`. It regenerates `home.png`, `editing.png`, and `configuration.png` at exactly 1280×800, uploads them as artifacts, and attaches them as individual Gitea release assets.
- **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`. - **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`.
- **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production. - **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production.
@@ -247,9 +254,9 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
| Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` | | Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` |
| chrome.storage.local access | `components/utils/StorageLocalManager.ts` | | chrome.storage.local access | `components/utils/StorageLocalManager.ts` |
| Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) | | Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) |
| Release packaging | `scripts/prepare_release.sh`, `scripts/check_virustotal.sh`, `.gitea/workflows/release.yaml` | | Build/release/PR pipelines | `scripts/prepare_release.sh`, `scripts/capture_screenshots.mjs`, `scripts/check_virustotal.sh`, `.gitea/workflows/pull-request.yaml`, `.gitea/workflows/release.yaml` |
| Docker build | `Dockerfile` | | Docker build | `Dockerfile` |
--- ---
_Last updated: 2026-07-08. Generated as a general project overview; not a coding-style guide._ _Last updated: 2026-07-10. Generated as a general project overview; not a coding-style guide._

View File

@@ -0,0 +1,190 @@
import { spawn } from 'node:child_process';
import { mkdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
const viewport = { width: 1280, height: 800 };
const host = '127.0.0.1';
const port = 4173;
const configuredBaseUrl = process.env.SCREENSHOT_BASE_URL?.replace(/\/+$/, '');
const baseUrl = configuredBaseUrl || `http://${host}:${port}`;
const outputDirectory = process.env.SCREENSHOT_OUTPUT_DIR || 'screenshots';
const demoDataPath = process.env.SCREENSHOT_DEMO_DATA || 'scripts/demoData.json';
const viteCli = path.resolve('node_modules/vite/bin/vite.js');
const imgurImageCache = new Map();
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const waitForServer = async (preview) => {
const deadline = Date.now() + 30_000;
let lastError;
while (Date.now() < deadline) {
if (previewError) throw previewError;
if (preview && preview.exitCode !== null) {
throw new Error(`Vite preview exited with code ${preview.exitCode}.`);
}
try {
const response = await fetch(baseUrl);
if (response.ok) return;
lastError = new Error(`Vite preview returned ${response.status}.`);
} catch (error) {
lastError = error;
}
await delay(250);
}
throw new Error(`Screenshot target did not respond at ${baseUrl}. ${lastError?.message || ''}`.trim());
};
const loadDemoData = async () => {
const encodedData = JSON.parse(await readFile(demoDataPath, 'utf8'));
if (!encodedData || typeof encodedData !== 'object' || Array.isArray(encodedData)) {
throw new Error(`${demoDataPath} must contain an object of base64-encoded localStorage values.`);
}
return Object.entries(encodedData).map(([key, value]) => {
if (typeof value !== 'string') {
throw new Error(`${demoDataPath} contains a non-string value for ${key}.`);
}
return { key, value: Buffer.from(value, 'base64').toString('utf8') };
});
};
const fulfillImgurRequest = async (route) => {
const url = route.request().url();
let image = imgurImageCache.get(url);
if (!image) {
try {
const response = await fetch(url);
if (!response.ok) {
await route.continue();
return;
}
image = {
body: Buffer.from(await response.arrayBuffer()),
contentType: response.headers.get('content-type') || 'image/jpeg',
};
imgurImageCache.set(url, image);
} catch {
await route.continue();
return;
}
}
await route.fulfill({
status: 200,
headers: { 'content-type': image.contentType },
body: image.body,
});
};
const stopPreview = async (preview) => {
if (preview.exitCode !== null) return;
const exited = new Promise((resolve) => preview.once('exit', resolve));
preview.kill('SIGTERM');
await Promise.race([exited, delay(5_000)]);
if (preview.exitCode === null) preview.kill('SIGKILL');
};
const assertPngDimensions = (image, filename) => {
const width = image.readUInt32BE(16);
const height = image.readUInt32BE(20);
if (width !== viewport.width || height !== viewport.height) {
throw new Error(`${filename} was generated at ${width}x${height}, expected ${viewport.width}x${viewport.height}.`);
}
};
const capture = async (page, filename) => {
const image = await page.screenshot({
path: path.join(outputDirectory, filename),
type: 'png',
fullPage: false,
scale: 'css',
animations: 'disabled',
});
assertPngDimensions(image, filename);
};
const loadPage = async (page) => {
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await page.locator('main').waitFor({ state: 'visible' });
await page.evaluate(() => document.fonts.ready);
await delay(1_500);
};
let previewError;
const preview = configuredBaseUrl
? undefined
: spawn(
process.execPath,
[viteCli, 'preview', '--host', host, '--port', String(port), '--strictPort'],
{ stdio: 'inherit' },
);
if (preview) {
preview.once('error', (error) => {
previewError = error;
});
}
try {
await waitForServer(preview);
await mkdir(outputDirectory, { recursive: true });
const demoData = await loadDemoData();
const browser = await chromium.launch();
const context = await browser.newContext({
viewport,
screen: viewport,
deviceScaleFactor: 1,
locale: 'en-US',
timezoneId: 'America/Sao_Paulo',
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
});
await context.route('https://i.imgur.com/**', fulfillImgurRequest);
await context.addInitScript((storageItems) => {
storageItems.forEach(({ key, value }) => localStorage.setItem(key, value));
}, demoData);
const page = await context.newPage();
try {
await loadPage(page);
await capture(page, 'home.png');
await page.getByRole('button', { name: 'Edit page' }).click();
await capture(page, 'editing.png');
await loadPage(page);
await page.getByRole('button', { name: 'Open configuration' }).click();
await page.getByRole('dialog').waitFor({ state: 'visible' });
await page.waitForFunction(
() => {
const drawer = document.querySelector('.liquid-drawer');
if (!drawer) return false;
const { left, right } = drawer.getBoundingClientRect();
return left < window.innerWidth && right <= window.innerWidth;
},
undefined,
{ timeout: 5_000 },
);
await capture(page, 'configuration.png');
} finally {
await context.close();
await browser.close();
}
} finally {
if (preview) await stopPreview(preview);
}

6
scripts/demoData.json Normal file
View File

@@ -0,0 +1,6 @@
{
"categories": "W3siaWQiOiIxIiwibmFtZSI6IlNlYXJjaCIsIndlYnNpdGVzIjpbeyJpZCI6IjEiLCJuYW1lIjoiR29vZ2xlIiwidXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbSIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1nb29nbGUuY29tJnN6PTEyOCIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQxODAxMDgiLCJuYW1lIjoiWW91VHViZSIsInVybCI6Imh0dHBzOi8vd3d3LnlvdXR1YmUuY29tLyIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy95b3V0dWJlLnN2ZyIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQyMjI2OTQiLCJuYW1lIjoiRHJpdmUiLCJ1cmwiOiJodHRwczovL2RyaXZlLmdvb2dsZS5jb20vZHJpdmUvdS8wL215LWRyaXZlIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dvb2dsZS1kcml2ZS5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDE0ODIwIiwibmFtZSI6IkdtYWlsIiwidXJsIjoiaHR0cHM6Ly9tYWlsLmdvb2dsZS5jb20vbWFpbC91LzAvP3RhYj13bSNpbmJveCIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9nbWFpbC5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDI3NTYwIiwibmFtZSI6Ikxhc3QuZm0iLCJ1cmwiOiJodHRwczovL3d3dy5sYXN0LmZtL2hvbWUiLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49d3d3Lmxhc3QuZm0mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjEifV19LHsiaWQiOiIxNzUzMDQ0NDQ2NzU1IiwibmFtZSI6IkNvZGUiLCJ3ZWJzaXRlcyI6W3siaWQiOiIxNzUzMDQ0NDU5MjcyIiwibmFtZSI6IkdpdGh1YiIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9mZWVkIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dpdGh1Yi1saWdodC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4ODcwNjgzIiwibmFtZSI6IkFXUyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9hd3MtbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ0NDY3NTUifSx7ImlkIjoiMTc4MzcyODkxNTg1MyIsIm5hbWUiOiJEb2NrZXIgSHViIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2RvY2tlci5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4OTkyMzkwIiwibmFtZSI6IkZpZ21hIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2ZpZ21hLnN2ZyIsImNhdGVnb3J5SWQiOiIxNzUzMDQ0NDQ2NzU1In1dfSx7ImlkIjoiMTc1MzA0NDY2ODAxMiIsIm5hbWUiOiJNZWRpYSIsIndlYnNpdGVzIjpbeyJpZCI6IjE3NTMwNDQ3MDYwMjQiLCJuYW1lIjoiTmV0ZmxpeCIsInVybCI6Imh0dHA6Ly90di5oYXZlbi8iLCJpY29uIjoiaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL2hvbWFyci1sYWJzL2Rhc2hib2FyZC1pY29ucy9zdmcvbmV0ZmxpeC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzUzMDQ0NzQwOTg2IiwibmFtZSI6IlByaW1lIiwidXJsIjoiaHR0cDovL29tdi5oYXZlbiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9wcmltZS12aWRlby1hbHQtZGFyay5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzgzNzI4Nzg0MjU2IiwibmFtZSI6IkhCTyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9oYm8tbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc1MzA0NDgyODg0MCIsIm5hbWUiOiJUcmFrdCIsInVybCI6Imh0dHBzOi8vYXBwLnRyYWt0LnR2LyIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1hcHAudHJha3QudHYmc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc4MzcyODcyOTQyNCIsIm5hbWUiOiJMZXR0ZXJib3hkIiwidXJsIjoiaHR0cHM6Ly9sZXR0ZXJib3hkLmNvbS8iLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49bGV0dGVyYm94ZC5jb20mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifV19XQ==",
"config": "eyJ0aXRsZSI6IiIsImN1cnJlbnRXYWxscGFwZXJzIjpbIkFic3RyYWN0IFJlZCJdLCJ3YWxscGFwZXJGcmVxdWVuY3kiOiIxZCIsIndhbGxwYXBlckJsdXIiOjAsIndhbGxwYXBlckJyaWdodG5lc3MiOjEwOCwid2FsbHBhcGVyT3BhY2l0eSI6MTAwLCJ0aXRsZVNpemUiOiJtZWRpdW0iLCJhbGlnbm1lbnQiOiJtaWRkbGUiLCJob3Jpem9udGFsQWxpZ25tZW50IjoibWlkZGxlIiwidGlsZVNpemUiOiJzbWFsbCIsImNsb2NrIjp7ImVuYWJsZWQiOnRydWUsInNpemUiOiJ0aW55IiwiZm9udCI6Im1vbm9zcGFjZSIsImZvcm1hdCI6Img6bW0gQSJ9LCJzZXJ2ZXJXaWRnZXQiOnsiZW5hYmxlZCI6dHJ1ZSwicGluZ0ZyZXF1ZW5jeSI6MTUsInNlcnZlcnMiOlt7ImlkIjoiMTc4MzcyOTE4MzU1MyIsIm5hbWUiOiJBZEd1YXJkIiwiYWRkcmVzcyI6Imh0dHBzOi8vZ29vZ2xlLmNvbSJ9LHsiaWQiOiIxNzgzNzI5MjI3NDQyIiwibmFtZSI6IlByb3htb3giLCJhZGRyZXNzIjoiaHR0cHM6Ly9nb29nbGUuY29tIn1dfX0=",
"userWallpapers": "W3sibmFtZSI6ImRhcmstYWJzdHJhY3QtMjU2MHgxNDQwLWNvbnRlbXBvcmFyeS1kZXNpZ24tc2xlZWstbGluZXMtMjY0MjYuanBnIn1d",
"wallpaperState": "eyJsYXN0V2FsbHBhcGVyQ2hhbmdlIjoiMjA0Ni0wNy0xMVQwMDoyMzozMS4zODdaIiwiY3VycmVudEluZGV4IjowfQ=="
}