feat: phase 5 distribution — tauri shell, pwa, desktop release artifacts
- apps/desktop: Tauri 2 thin shell; connect screen stores the deployment URL, webview renders the remote console, tray with Show/Change Server/ Quit, close-to-tray, single-instance - frontend: vite-plugin-pwa generates manifest + Workbox SW (shell precache, /api /auth /mailpit NetworkOnly), install banner on beforeinstallprompt, rasterized PWA icons - release.yml: desktop bundle matrix (linux/macos/windows) attached to GitHub Releases - workspace 0.5.0; Dockerfiles copy the desktop workspace manifest Authored By: TDvorak <[email protected]>
No files matched your search
@@ -84,6 +84,42 @@ jobs:
|
|||||||
path: dist-release/
|
path: dist-release/
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
desktop:
|
||||||
|
name: Desktop bundle (${{ matrix.name }})
|
||||||
|
needs: verify
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { os: ubuntu-22.04, name: linux }
|
||||||
|
- { os: macos-latest, name: macos }
|
||||||
|
- { os: windows-latest, name: windows }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
- run: npm ci
|
||||||
|
- name: Install Linux webview dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Build Tauri bundles
|
||||||
|
run: npm run bundle --workspace @primora/desktop
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: desktop-${{ matrix.name }}
|
||||||
|
path: |
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.AppImage
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.deb
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.rpm
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.dmg
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.msi
|
||||||
|
apps/desktop/src-tauri/target/release/bundle/**/*.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
npm-packages:
|
npm-packages:
|
||||||
name: Pack npm tarballs
|
name: Pack npm tarballs
|
||||||
needs: verify
|
needs: verify
|
||||||
@@ -108,7 +144,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
name: GitHub Release
|
name: GitHub Release
|
||||||
needs: [images, cli-binaries, npm-packages]
|
needs: [images, cli-binaries, desktop, npm-packages]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ bin
|
|||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
apps/frontend/dist
|
apps/frontend/dist
|
||||||
|
apps/frontend/dev-dist
|
||||||
apps/auth/dist
|
apps/auth/dist
|
||||||
packages/api-client/generated
|
packages/api-client/generated
|
||||||
|
dist-release
|
||||||
|
|
||||||
|
# Rust / Tauri
|
||||||
|
apps/desktop/src-tauri/target
|
||||||
|
apps/desktop/src-tauri/gen/schemas
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
infra/data
|
infra/data
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-09-16
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **Desktop app** (`apps/desktop`) — Tauri 2 thin shell for Linux/Windows/macOS. First run shows a connect screen asking for the deployment URL (validated, stored in `~/.config/dev.tdvorak.primora/config.json`); the webview then renders the same SolidJS bundle the server serves. System-tray icon (Show / Change Server / Quit), close-to-tray, single-instance. No embedded backend or database — the self-hosted model is preserved.
|
||||||
|
- **PWA support** — `vite-plugin-pwa` generates a Workbox service worker and `manifest.webmanifest` in every frontend build. The app shell (JS/CSS/icons/index.html) is precached; `/api`, `/auth`, and `/mailpit` are `NetworkOnly` and excluded from the SPA navigation fallback; Google Fonts assets are cached for a year. Installability icons (192/512/maskable + apple-touch-icon) rasterized from the existing SVG mark.
|
||||||
|
- **Install prompt** — `PwaInstallBanner` captures `beforeinstallprompt` and offers an in-app install action inside the shell; dismissal persists in `localStorage`.
|
||||||
|
- **Desktop release artifacts** — `release.yml` gains a `desktop` job (ubuntu/macos/windows matrix) producing AppImage/deb/rpm, dmg, and msi/nsis bundles, attached to the GitHub Release alongside the existing images and CLI binaries.
|
||||||
|
|
||||||
- **Connector framework** — `core.integrations` (migration 00005) stores per-project external services with type, base URL, `has_credentials` flag, health status/`last_health_at`. Credentials are AES-256-GCM encrypted at rest (`PRIMORA_ENCRYPTION_KEY`, hex/base64, 32 bytes; dev fallback outside production) and are never serialized to the client. Endpoints under `/projects/:id/integrations`: list, create, delete, `POST …/test` (5s health check against the base URL with stored credentials).
|
- **Connector framework** — `core.integrations` (migration 00005) stores per-project external services with type, base URL, `has_credentials` flag, health status/`last_health_at`. Credentials are AES-256-GCM encrypted at rest (`PRIMORA_ENCRYPTION_KEY`, hex/base64, 32 bytes; dev fallback outside production) and are never serialized to the client. Endpoints under `/projects/:id/integrations`: list, create, delete, `POST …/test` (5s health check against the base URL with stored credentials).
|
||||||
- **Rybbit connector** — `GET …/integrations/:id/analytics` proxies a self-hosted Rybbit instance server-side (`/api/organizations` site discovery, `/api/sites/:site/overview`, time series, top pages/referrers) and returns a normalized shape. The API key never leaves the server; read-only, no tracking proxy. Site selection via `?site=` or the integration's configured `site_id`.
|
- **Rybbit connector** — `GET …/integrations/:id/analytics` proxies a self-hosted Rybbit instance server-side (`/api/organizations` site discovery, `/api/sites/:site/overview`, time series, top pages/referrers) and returns a normalized shape. The API key never leaves the server; read-only, no tracking proxy. Site selection via `?site=` or the integration's configured `site_id`.
|
||||||
- **Outbound webhooks** — `core.webhooks` + `core.webhook_deliveries`. Endpoints: list/create/update/delete under `/projects/:id/webhooks`, `GET …/deliveries`, `POST …/test`. Events: `issue.created` (new error fingerprint on ingest), `deploy.marker` (`POST …/deploy-markers`, records version/ref/environment/note in the audit log), `webhook.test`. HTTPS required for public targets; plain HTTP allowed for private/self-hosted sinks. Deliveries are signed `X-Primora-Signature: sha256=<hmac-sha256(body)>` and retried by an in-process dispatcher (buffered queue, 3 attempts, quadratic backoff, 10s timeout, crash recovery on boot). Generated secrets are returned exactly once.
|
- **Outbound webhooks** — `core.webhooks` + `core.webhook_deliveries`. Endpoints: list/create/update/delete under `/projects/:id/webhooks`, `GET …/deliveries`, `POST …/test`. Events: `issue.created` (new error fingerprint on ingest), `deploy.marker` (`POST …/deploy-markers`, records version/ref/environment/note in the audit log), `webhook.test`. HTTPS required for public targets; plain HTTP allowed for private/self-hosted sinks. Deliveries are signed `X-Primora-Signature: sha256=<hmac-sha256(body)>` and retried by an in-process dispatcher (buffered queue, 3 attempts, quadratic backoff, 10s timeout, crash recovery on boot). Generated secrets are returned exactly once.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Browser ──▶ Nginx ──▶ Frontend (SolidJS + Vite)
|
|||||||
- **Auth** — email/password plus optional GitHub, Google, Discord, and Microsoft OAuth via Better Auth. JWTs minted by the auth service are verified by the Go API against JWKS.
|
- **Auth** — email/password plus optional GitHub, Google, Discord, and Microsoft OAuth via Better Auth. JWTs minted by the auth service are verified by the Go API against JWKS.
|
||||||
- **Storage** — S3-style buckets and objects backed by your local filesystem, with public/private visibility and downloadable URLs.
|
- **Storage** — S3-style buckets and objects backed by your local filesystem, with public/private visibility and downloadable URLs.
|
||||||
- **Collections** — schema-flexible JSON documents stored in Postgres JSONB.
|
- **Collections** — schema-flexible JSON documents stored in Postgres JSONB.
|
||||||
- **API keys** — `pk_live_`/`pk_test_` credentials with prefixes; secrets are shown once.
|
- **API keys** — `prm_<prefix>_<secret>` credentials; secrets are shown once.
|
||||||
- **Audit log** — every mutating request recorded with actor, resource, request ID, and timestamp; CSV/JSON export from the dashboard.
|
- **Audit log** — every mutating request recorded with actor, resource, request ID, and timestamp; CSV/JSON export from the dashboard.
|
||||||
- **Generated client** — the TypeScript client is generated from `apps/backend/openapi/openapi.yaml`, so the API contract is the source of truth.
|
- **Generated client** — the TypeScript client is generated from `apps/backend/openapi/openapi.yaml`, so the API contract is the source of truth.
|
||||||
- **Demo mode** — a fully client-side workspace (`?demo=true` or `VITE_DEMO_MODE=true`) for trying the UI without a backend.
|
- **Demo mode** — a fully client-side workspace (`?demo=true` or `VITE_DEMO_MODE=true`) for trying the UI without a backend.
|
||||||
@@ -77,6 +77,17 @@ node apps/cli/dist/cli.js login # or: npx primora login once published
|
|||||||
|
|
||||||
Supports session sign-in (`primora login`) and API-key mode (`primora login --api-key prm_…` or `PRIMORA_API_KEY`). Context lives in `~/.config/primora/config.json`; `primora use` picks org + project interactively. Commands: `orgs`, `projects`, `buckets`, `objects` (list/upload/download/rm), `keys`, `audit list --follow`. Every command accepts `--json`.
|
Supports session sign-in (`primora login`) and API-key mode (`primora login --api-key prm_…` or `PRIMORA_API_KEY`). Context lives in `~/.config/primora/config.json`; `primora use` picks org + project interactively. Commands: `orgs`, `projects`, `buckets`, `objects` (list/upload/download/rm), `keys`, `audit list --follow`. Every command accepts `--json`.
|
||||||
|
|
||||||
|
## Desktop & phone
|
||||||
|
|
||||||
|
- **Desktop** — `apps/desktop` is a Tauri 2 shell (Linux/Windows/macOS). First
|
||||||
|
run asks for your deployment URL, then renders the same dashboard in a
|
||||||
|
webview with a system-tray presence. Nothing is bundled; your server does the
|
||||||
|
work. See [apps/desktop/README.md](apps/desktop/README.md).
|
||||||
|
- **Phone / PWA** — the dashboard ships a web manifest and service worker.
|
||||||
|
Open your deployment in a mobile browser and "Add to Home Screen"; the shell
|
||||||
|
is precached, API traffic always goes to the network. Chromium desktop
|
||||||
|
browsers get an in-app install banner.
|
||||||
|
|
||||||
## Quality gate
|
## Quality gate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -94,6 +105,8 @@ apps/
|
|||||||
auth/ Better Auth on Hono (sessions, JWT, OAuth, mail)
|
auth/ Better Auth on Hono (sessions, JWT, OAuth, mail)
|
||||||
frontend/ SolidJS + Tailwind dashboard
|
frontend/ SolidJS + Tailwind dashboard
|
||||||
cli/ `primora` CLI — login, context, projects, buckets, objects, keys, audit
|
cli/ `primora` CLI — login, context, projects, buckets, objects, keys, audit
|
||||||
|
mcp/ `@primora/mcp` — MCP stdio server exposing `primora_*` tools
|
||||||
|
desktop/ Tauri 2 shell — connects to a deployment URL, tray presence
|
||||||
packages/
|
packages/
|
||||||
api-client/ OpenAPI-generated TypeScript client (do not hand-edit)
|
api-client/ OpenAPI-generated TypeScript client (do not hand-edit)
|
||||||
shared-types/
|
shared-types/
|
||||||
|
|||||||
@@ -99,18 +99,23 @@ IMS (`~/Desktop/PROG+HTML/IMS`) is discontinued and folded in — same stack
|
|||||||
(`X-Primora-Signature`), in-process dispatcher with backoff retries, HTTPS
|
(`X-Primora-Signature`), in-process dispatcher with backoff retries, HTTPS
|
||||||
for public targets (HTTP allowed for private/self-hosted sinks).
|
for public targets (HTTP allowed for private/self-hosted sinks).
|
||||||
|
|
||||||
## Phase 5 — Distribution
|
## Phase 5 — Distribution ✅
|
||||||
|
|
||||||
- **Desktop app** — Tauri 2 shell (Linux/Windows/macOS). Thin client: asks for
|
- ~~**Desktop app**~~ — `apps/desktop`, a Tauri 2 shell (Linux/Windows/macOS).
|
||||||
the deployment URL on first run, renders the same SolidJS bundle, keeps a
|
Thin client: asks for the deployment URL on first run, renders the same
|
||||||
system-tray presence. No embedded stack — self-hosted model preserved.
|
SolidJS bundle, keeps a system-tray presence (Show / Change Server / Quit,
|
||||||
- **Phone** — PWA (manifest, service worker, install prompt) first; native
|
close-to-tray, single-instance). No embedded stack — self-hosted model
|
||||||
shells via Capacitor only if push notifications become a requirement.
|
preserved.
|
||||||
- **Versioning** — `v0.x` minor-per-phase until API surface stabilizes,
|
- ~~**Phone**~~ — PWA shipped: `manifest.webmanifest` + Workbox service worker
|
||||||
`v1.0.0` when Phase 3 lands. Every release: GitHub Release notes +
|
precache the shell (`/api`, `/auth`, `/mailpit` stay `NetworkOnly`), plus an
|
||||||
tagged images + CLI binaries.
|
in-app install banner on `beforeinstallprompt`. Native shells via Capacitor
|
||||||
- **Docs site** — README stays canonical; a docs/ folder or site once the API
|
remain deferred until push notifications are a real requirement.
|
||||||
settles.
|
- ~~**Versioning**~~ — `v0.x` minor-per-phase until the API surface stabilizes.
|
||||||
|
Release pipeline (Phase 1) now also ships desktop bundles: tag → GitHub
|
||||||
|
Release notes + ghcr.io images + CLI/MCP binaries + desktop installers.
|
||||||
|
- **Docs site** — deferred on purpose. README stays canonical; the OpenAPI
|
||||||
|
contract is the machine-readable source. The API still changes every phase —
|
||||||
|
a docs site now would document a moving target. Revisit when `v1.0.0` lands.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ WORKDIR /workspace
|
|||||||
COPY package.json package-lock.json tsconfig.base.json ./
|
COPY package.json package-lock.json tsconfig.base.json ./
|
||||||
COPY apps/auth/package.json ./apps/auth/package.json
|
COPY apps/auth/package.json ./apps/auth/package.json
|
||||||
COPY apps/cli/package.json ./apps/cli/package.json
|
COPY apps/cli/package.json ./apps/cli/package.json
|
||||||
|
COPY apps/desktop/package.json ./apps/desktop/package.json
|
||||||
COPY apps/frontend/package.json ./apps/frontend/package.json
|
COPY apps/frontend/package.json ./apps/frontend/package.json
|
||||||
COPY apps/mcp/package.json ./apps/mcp/package.json
|
COPY apps/mcp/package.json ./apps/mcp/package.json
|
||||||
COPY packages/api-client/package.json ./packages/api-client/package.json
|
COPY packages/api-client/package.json ./packages/api-client/package.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/auth",
|
"name": "@primora/auth",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/cli",
|
"name": "@primora/cli",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"description": "Primora command-line client",
|
"description": "Primora command-line client",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"picocolors": "^1.1.1"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@primora/api-client": "0.4.0",
|
"@primora/api-client": "0.5.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# @primora/desktop
|
||||||
|
|
||||||
|
Tauri 2 shell around a self-hosted Primora deployment. On first run it asks for
|
||||||
|
the deployment URL; after that the webview renders the same SolidJS console the
|
||||||
|
server serves. Nothing is embedded — no backend, no database. The self-hosted
|
||||||
|
model is preserved.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Rust (rustup) and Node.js.
|
||||||
|
- Linux webview deps:
|
||||||
|
`sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev`
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev --workspace @primora/desktop # dev window (debug build)
|
||||||
|
npm run bundle --workspace @primora/desktop # release bundles → src-tauri/target/release/bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Config lives at `~/.config/dev.tdvorak.primora/config.json` — one field, `server_url`.
|
||||||
|
- System tray: **Show Primora**, **Change Server…** (clears the URL, back to the
|
||||||
|
connect screen), **Quit**. Closing the window hides to the tray; a second
|
||||||
|
launch focuses the existing window (single-instance).
|
||||||
|
- The connect screen only checks that the host answers; if the URL turns out not
|
||||||
|
to be a Primora deployment, use the tray's Change Server.
|
||||||
|
- macOS blocks plain-HTTP loads to non-localhost hosts (ATS). Use HTTPS, or
|
||||||
|
`http://localhost` for a local deployment.
|
||||||
|
- Deep links / push notifications: deliberately omitted — nothing needs them yet.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@primora/desktop",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Primora desktop shell — thin Tauri client for a self-hosted deployment",
|
||||||
|
"scripts": {
|
||||||
|
"tauri": "tauri",
|
||||||
|
"dev": "tauri dev",
|
||||||
|
"bundle": "tauri build",
|
||||||
|
"check": "cargo check --manifest-path src-tauri/Cargo.toml"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.11.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "primora-desktop"
|
||||||
|
version = "0.5.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Primora desktop shell — thin client for a self-hosted Primora deployment"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "primora_desktop_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||||
|
tauri-plugin-single-instance = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capabilities for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 961 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,125 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{
|
||||||
|
menu::{Menu, MenuItem},
|
||||||
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
|
AppHandle, Manager, WindowEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Persisted shell state — one field, the deployment to load.
|
||||||
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
struct DesktopConfig {
|
||||||
|
server_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
|
app.path()
|
||||||
|
.app_config_dir()
|
||||||
|
.map(|dir| dir.join("config.json"))
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config(app: &AppHandle) -> DesktopConfig {
|
||||||
|
config_path(app)
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| std::fs::read_to_string(path).ok())
|
||||||
|
.and_then(|raw| serde_json::from_str(&raw).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config(app: &AppHandle, config: &DesktopConfig) -> Result<(), String> {
|
||||||
|
let path = config_path(app)?;
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
let raw = serde_json::to_string_pretty(config).map_err(|e| e.to_string())?;
|
||||||
|
std::fs::write(path, raw).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_server_url(app: AppHandle) -> Option<String> {
|
||||||
|
load_config(&app).server_url
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_server_url(app: AppHandle, url: String) -> Result<(), String> {
|
||||||
|
let parsed = tauri::Url::parse(url.trim()).map_err(|_| "That is not a valid URL.".to_string())?;
|
||||||
|
match parsed.scheme() {
|
||||||
|
"http" | "https" => {}
|
||||||
|
_ => return Err("Only http:// and https:// URLs are supported.".to_string()),
|
||||||
|
}
|
||||||
|
if parsed.host_str().is_none() {
|
||||||
|
return Err("The URL needs a host.".to_string());
|
||||||
|
}
|
||||||
|
let mut normalized = parsed.to_string();
|
||||||
|
while normalized.ends_with('/') {
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
save_config(&app, &DesktopConfig { server_url: Some(normalized) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_main(app: &AppHandle) {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
|
show_main(app);
|
||||||
|
}))
|
||||||
|
.invoke_handler(tauri::generate_handler![get_server_url, set_server_url])
|
||||||
|
.setup(|app| {
|
||||||
|
let show = MenuItem::with_id(app, "show", "Show Primora", true, None::<&str>)?;
|
||||||
|
let change = MenuItem::with_id(app, "change_server", "Change Server…", true, None::<&str>)?;
|
||||||
|
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||||
|
let menu = Menu::with_items(app, &[&show, &change, &quit])?;
|
||||||
|
|
||||||
|
let mut tray = TrayIconBuilder::new()
|
||||||
|
.menu(&menu)
|
||||||
|
.tooltip("Primora")
|
||||||
|
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||||
|
"show" => show_main(app),
|
||||||
|
"change_server" => {
|
||||||
|
// Clearing the URL and restarting lands back on the connect screen.
|
||||||
|
let _ = save_config(app, &DesktopConfig { server_url: None });
|
||||||
|
app.restart();
|
||||||
|
}
|
||||||
|
"quit" => app.exit(0),
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if let TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
show_main(tray.app_handle());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(icon) = app.default_window_icon() {
|
||||||
|
tray = tray.icon(icon.clone());
|
||||||
|
}
|
||||||
|
let _tray = tray.build(app)?;
|
||||||
|
|
||||||
|
// Close hides to the tray instead of quitting — Quit is on the menu.
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let handle = window.clone();
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = handle.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Primora desktop");
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
primora_desktop_lib::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Primora",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"identifier": "dev.tdvorak.primora",
|
||||||
|
"build": {
|
||||||
|
"frontendDist": "../ui"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"title": "Primora",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 800,
|
||||||
|
"minWidth": 420,
|
||||||
|
"minHeight": 640,
|
||||||
|
"center": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https: http:; img-src 'self' data:; style-src 'self'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"shortDescription": "Primora desktop client",
|
||||||
|
"longDescription": "Thin desktop shell for a self-hosted Primora deployment. Renders the console served by your own server.",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/[email protected]",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="14" fill="#f4693a"/>
|
||||||
|
<g fill="#ffffff">
|
||||||
|
<rect x="15" y="14" width="34" height="10" rx="5"/>
|
||||||
|
<rect x="15" y="27" width="23" height="10" rx="5" opacity="0.7"/>
|
||||||
|
<rect x="15" y="40" width="10" height="10" rx="5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,84 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="color-scheme" content="dark" />
|
||||||
|
<title>Primora</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="icon.svg" />
|
||||||
|
<link rel="stylesheet" href="style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="card">
|
||||||
|
<img src="icon.svg" class="logo" alt="" />
|
||||||
|
<h1>Primora</h1>
|
||||||
|
<p class="sub">Connect to your self-hosted deployment.</p>
|
||||||
|
<form id="form">
|
||||||
|
<input
|
||||||
|
id="url"
|
||||||
|
type="text"
|
||||||
|
inputmode="url"
|
||||||
|
placeholder="https://primora.example.com"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button type="submit" id="go">Connect</button>
|
||||||
|
</form>
|
||||||
|
<p id="err" class="err" hidden></p>
|
||||||
|
<p class="hint">The desktop app renders the console served by your own server. Nothing is hosted by us.</p>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const tauri = window.__TAURI__;
|
||||||
|
const form = document.getElementById("form");
|
||||||
|
const input = document.getElementById("url");
|
||||||
|
const button = document.getElementById("go");
|
||||||
|
const err = document.getElementById("err");
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
err.textContent = msg;
|
||||||
|
err.hidden = false;
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "Connect";
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!tauri) {
|
||||||
|
fail("This page is meant to run inside the Primora desktop app.");
|
||||||
|
} else {
|
||||||
|
// A saved deployment URL means "skip the form" on every launch.
|
||||||
|
const saved = await tauri.core.invoke("get_server_url").catch(() => null);
|
||||||
|
if (saved) location.replace(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener("submit", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
err.hidden = true;
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "Checking…";
|
||||||
|
|
||||||
|
let url = input.value.trim();
|
||||||
|
if (!/^https?:\/\//i.test(url)) url = `https://${url}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await tauri.core.invoke("set_server_url", { url });
|
||||||
|
} catch (e) {
|
||||||
|
fail(String(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reachability probe — opaque (no-cors), so it proves the host answers,
|
||||||
|
// not that it is a Primora deployment. A wrong-but-alive URL is recoverable
|
||||||
|
// via the tray menu's "Change server".
|
||||||
|
try {
|
||||||
|
await fetch(url, { mode: "no-cors", signal: AbortSignal.timeout(8000) });
|
||||||
|
} catch {
|
||||||
|
fail("Server did not respond. Check the URL and that the deployment is up.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location.replace(url);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: #0b0d11;
|
||||||
|
color: #e7e7ea;
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: min(24rem, calc(100vw - 3rem));
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
background: #14161b;
|
||||||
|
border: 1px solid #26292f;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
color: #9a9aa3;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 0.65rem 0.8rem;
|
||||||
|
background: #0b0d11;
|
||||||
|
border: 1px solid #2e3239;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: #f4693a;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.65rem 0.8rem;
|
||||||
|
background: #f4693a;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err {
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
color: #ef6b6b;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
color: #6d6d76;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json tsconfig.base.json ./
|
COPY package.json package-lock.json tsconfig.base.json ./
|
||||||
COPY apps/auth/package.json ./apps/auth/package.json
|
COPY apps/auth/package.json ./apps/auth/package.json
|
||||||
COPY apps/cli/package.json ./apps/cli/package.json
|
COPY apps/cli/package.json ./apps/cli/package.json
|
||||||
|
COPY apps/desktop/package.json ./apps/desktop/package.json
|
||||||
COPY apps/frontend/package.json ./apps/frontend/package.json
|
COPY apps/frontend/package.json ./apps/frontend/package.json
|
||||||
COPY apps/mcp/package.json ./apps/mcp/package.json
|
COPY apps/mcp/package.json ./apps/mcp/package.json
|
||||||
COPY packages/api-client/package.json ./packages/api-client/package.json
|
COPY packages/api-client/package.json ./packages/api-client/package.json
|
||||||
|
|||||||
@@ -10,6 +10,11 @@
|
|||||||
<title>Primora</title>
|
<title>Primora</title>
|
||||||
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Primora" />
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/frontend",
|
"name": "@primora/frontend",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@primora/api-client": "0.4.0",
|
"@primora/api-client": "0.5.0",
|
||||||
"@primora/shared-types": "0.4.0",
|
"@primora/shared-types": "0.5.0",
|
||||||
"better-auth": "^1.5.6",
|
"better-auth": "^1.5.6",
|
||||||
"solid-js": "^1.9.12"
|
"solid-js": "^1.9.12"
|
||||||
},
|
},
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"vite": "^8.0.3",
|
"vite": "^8.0.3",
|
||||||
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
"vite-plugin-solid": "^2.11.8",
|
"vite-plugin-solid": "^2.11.8",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" fill="#f4693a"/>
|
||||||
|
<g fill="#ffffff" transform="translate(6.4 6.4) scale(0.8)">
|
||||||
|
<rect x="15" y="14" width="34" height="10" rx="5"/>
|
||||||
|
<rect x="15" y="27" width="23" height="10" rx="5" opacity="0.7"/>
|
||||||
|
<rect x="15" y="40" width="10" height="10" rx="5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 368 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -31,6 +31,7 @@ import {
|
|||||||
NetworkError,
|
NetworkError,
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
ProjectDashboard,
|
ProjectDashboard,
|
||||||
|
PwaInstallBanner,
|
||||||
type PaletteCommand,
|
type PaletteCommand,
|
||||||
type ViewType,
|
type ViewType,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
@@ -1500,6 +1501,7 @@ export default function App() {
|
|||||||
<Show when={platformMessage()}>
|
<Show when={platformMessage()}>
|
||||||
<div class="message message-neutral mb-5">{platformMessage()}</div>
|
<div class="message message-neutral mb-5">{platformMessage()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
<PwaInstallBanner />
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={platform() || isDemo}
|
when={platform() || isDemo}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Show, createSignal, onCleanup, onMount } from "solid-js";
|
||||||
|
|
||||||
|
// Chromium fires `beforeinstallprompt` once manifest + service worker pass
|
||||||
|
// installability checks. Stash the event and let the banner trigger it —
|
||||||
|
// the native mini-infobar is easy to miss.
|
||||||
|
interface BeforeInstallPromptEvent extends Event {
|
||||||
|
prompt(): Promise<void>;
|
||||||
|
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISMISS_KEY = "primora_pwa_install_dismissed";
|
||||||
|
|
||||||
|
export function PwaInstallBanner() {
|
||||||
|
const [deferred, setDeferred] = createSignal<BeforeInstallPromptEvent | null>(null);
|
||||||
|
const [dismissed, setDismissed] = createSignal(localStorage.getItem(DISMISS_KEY) === "1");
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const handler = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
// SAFETY: the listener is only attached to "beforeinstallprompt", whose
|
||||||
|
// event always carries prompt()/userChoice.
|
||||||
|
setDeferred(event as BeforeInstallPromptEvent);
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeinstallprompt", handler);
|
||||||
|
onCleanup(() => window.removeEventListener("beforeinstallprompt", handler));
|
||||||
|
});
|
||||||
|
|
||||||
|
const install = async () => {
|
||||||
|
const event = deferred();
|
||||||
|
if (!event) return;
|
||||||
|
await event.prompt();
|
||||||
|
if ((await event.userChoice).outcome === "accepted") setDeferred(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
localStorage.setItem(DISMISS_KEY, "1");
|
||||||
|
setDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={deferred() && !dismissed()}>
|
||||||
|
<div
|
||||||
|
class="message message-info mb-5"
|
||||||
|
style="display:flex;align-items:center;justify-content:space-between;gap:1rem"
|
||||||
|
>
|
||||||
|
<span>Install Primora as an app for quick access from your launcher.</span>
|
||||||
|
<span style="display:flex;gap:0.5rem;flex-shrink:0">
|
||||||
|
<button class="btn btn-primary btn-sm" onClick={install}>
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" onClick={dismiss}>
|
||||||
|
Not now
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ export { Modal, ModalFooter } from "./Modal";
|
|||||||
export { NotificationCenter, addNotification, removeNotification, clearNotifications, notify } from "./NotificationCenter";
|
export { NotificationCenter, addNotification, removeNotification, clearNotifications, notify } from "./NotificationCenter";
|
||||||
export { OnboardingModal } from "./OnboardingModal";
|
export { OnboardingModal } from "./OnboardingModal";
|
||||||
export { Progress, CircularProgress, Spinner } from "./Progress";
|
export { Progress, CircularProgress, Spinner } from "./Progress";
|
||||||
|
export { PwaInstallBanner } from "./PwaInstallBanner";
|
||||||
export { ProjectDashboard } from "./ProjectDashboard";
|
export { ProjectDashboard } from "./ProjectDashboard";
|
||||||
export { Table, DataTable, Pagination } from "./Table";
|
export { Table, DataTable, Pagination } from "./Table";
|
||||||
export { Tabs, TabPanel } from "./Tabs";
|
export { Tabs, TabPanel } from "./Tabs";
|
||||||
|
|||||||
@@ -1,8 +1,56 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import solidPlugin from "vite-plugin-solid";
|
import solidPlugin from "vite-plugin-solid";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solidPlugin()],
|
plugins: [
|
||||||
|
solidPlugin(),
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
injectRegister: "auto",
|
||||||
|
manifest: {
|
||||||
|
name: "Primora",
|
||||||
|
short_name: "Primora",
|
||||||
|
description:
|
||||||
|
"Self-hosted backend console: storage, collections, API keys, audit logs.",
|
||||||
|
theme_color: "#0b0d11",
|
||||||
|
background_color: "#0b0d11",
|
||||||
|
display: "standalone",
|
||||||
|
start_url: "/",
|
||||||
|
icons: [
|
||||||
|
{ src: "pwa-192x192.png", sizes: "192x192", type: "image/png" },
|
||||||
|
{ src: "pwa-512x512.png", sizes: "512x512", type: "image/png" },
|
||||||
|
{
|
||||||
|
src: "maskable-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
// Cache the app shell only. /api, /auth and /mailpit are live backends —
|
||||||
|
// never let a service worker answer for them.
|
||||||
|
navigateFallback: "/index.html",
|
||||||
|
navigateFallbackDenylist: [/^\/(api|auth|mailpit)(\/|$)/],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /\/(api|auth|mailpit)(\/|$)/,
|
||||||
|
handler: "NetworkOnly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Geist font files + CSS from Google Fonts — long-lived static assets.
|
||||||
|
urlPattern: /^https:\/\/fonts\.(googleapis|gstatic)\.com\//,
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "primora-fonts",
|
||||||
|
expiration: { maxEntries: 20, maxAgeSeconds: 60 * 60 * 24 * 365 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: Number(process.env.FRONTEND_PORT ?? 3000),
|
port: Number(process.env.FRONTEND_PORT ?? 3000),
|
||||||
@@ -15,4 +63,3 @@ export default defineConfig({
|
|||||||
target: "esnext",
|
target: "esnext",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/mcp",
|
"name": "@primora/mcp",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@primora/api-client": "0.4.0",
|
"@primora/api-client": "0.5.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "primora",
|
"name": "primora",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "[email protected]",
|
"packageManager": "[email protected]",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/api-client",
|
"name": "@primora/api-client",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/client",
|
"name": "@primora/client",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/shared-types",
|
"name": "@primora/shared-types",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||