Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bcdc0888f | ||
|
|
fd451d219d | ||
|
|
a86f5f7199 | ||
|
|
2fccb7b405 | ||
|
|
0efabbdc2e | ||
|
|
294e72e6ff | ||
|
|
15f4dc9bac | ||
|
|
867df98958 | ||
|
|
e66d7ac102 | ||
|
|
84a6f8d41c | ||
|
|
08b0f79f07 | ||
|
|
9611567daa | ||
|
|
0bc5110e05 | ||
|
|
2433697951 | ||
|
|
341354ecb9 | ||
|
|
37e6638854 | ||
|
|
7bd4e8d05f | ||
|
|
7a66b70c26 |
@@ -140,13 +140,19 @@ jobs:
|
|||||||
|
|
||||||
# Ensure the release exists (idempotent — both jobs may call this).
|
# Ensure the release exists (idempotent — both jobs may call this).
|
||||||
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
if [ -z "$REL_ID" ]; then
|
if [ -z "$REL_ID" ]; then
|
||||||
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
BODY="Gridline ${TAG#v} - Linux and Windows installers."
|
||||||
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
||||||
"${API}/releases" \
|
"${API}/releases" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$REL_ID" ]; then
|
||||||
|
# A parallel job may have created the release between our GET and
|
||||||
|
# POST (create returns 422). Re-fetch by tag and upload to it.
|
||||||
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
||||||
@@ -189,7 +195,13 @@ jobs:
|
|||||||
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
URL="https://get.enterprisedb.com/postgresql/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
URL="https://get.enterprisedb.com/postgresql/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
||||||
|
CACHE="/c/ci/cache/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
||||||
|
if [ -f "$CACHE" ]; then
|
||||||
|
echo "Using cached PG zip: $CACHE"
|
||||||
|
cp "$CACHE" /tmp/pg.zip
|
||||||
|
else
|
||||||
curl -fsSL "$URL" -o /tmp/pg.zip
|
curl -fsSL "$URL" -o /tmp/pg.zip
|
||||||
|
fi
|
||||||
EXPECTED="3508d8f085bc3980f38211a82e3f31e5fcae9952105d3dc2f8be67b64a822baa"
|
EXPECTED="3508d8f085bc3980f38211a82e3f31e5fcae9952105d3dc2f8be67b64a822baa"
|
||||||
echo "$EXPECTED /tmp/pg.zip" | sha256sum -c -
|
echo "$EXPECTED /tmp/pg.zip" | sha256sum -c -
|
||||||
sha256sum /tmp/pg.zip
|
sha256sum /tmp/pg.zip
|
||||||
@@ -205,16 +217,34 @@ jobs:
|
|||||||
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/mysql_tools"
|
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/mysql_tools"
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
URL="https://archive.mariadb.org/mariadb-${MARIADB_VER}/winx64-packages/mariadb-${MARIADB_VER}-winx64.zip"
|
URL="https://archive.mariadb.org/mariadb-${MARIADB_VER}/winx64-packages/mariadb-${MARIADB_VER}-winx64.zip"
|
||||||
|
CACHE="/c/ci/cache/mariadb-${MARIADB_VER}-winx64.zip"
|
||||||
|
if [ -f "$CACHE" ]; then
|
||||||
|
echo "Using cached MariaDB zip: $CACHE"
|
||||||
|
cp "$CACHE" /tmp/mariadb.zip
|
||||||
|
else
|
||||||
curl -fsSL "$URL" -o /tmp/mariadb.zip
|
curl -fsSL "$URL" -o /tmp/mariadb.zip
|
||||||
|
fi
|
||||||
sha256sum /tmp/mariadb.zip
|
sha256sum /tmp/mariadb.zip
|
||||||
unzip -o /tmp/mariadb.zip -d /tmp/mariadb
|
unzip -o /tmp/mariadb.zip -d /tmp/mariadb
|
||||||
WINROOT="/tmp/mariadb/mariadb-${MARIADB_VER}-winx64"
|
WINROOT="/tmp/mariadb/mariadb-${MARIADB_VER}-winx64"
|
||||||
cp "$WINROOT"/bin/mariadb-dump.exe "$WINROOT"/bin/mariadb.exe "$OUT"/
|
cp "$WINROOT"/bin/mariadb-dump.exe "$WINROOT"/bin/mariadb.exe "$OUT"/
|
||||||
cp "$WINROOT"/lib/libmariadb.dll "$OUT"/
|
cp "$WINROOT"/lib/libmariadb.dll "$OUT"/
|
||||||
|
|
||||||
|
# rustc resolves `link.exe` via PATH for the windows-msvc target, but Git
|
||||||
|
# for Windows ships GNU coreutils with its own `link.exe` that shadows
|
||||||
|
# MSVC's linker (`link: extra operand ...`). Load the MSVC dev
|
||||||
|
# environment so PATH/LIB/INCLUDE point at the real MSVC toolchain.
|
||||||
|
# Fails fast here (instead of at link time) if the toolchain is missing.
|
||||||
|
- name: Set up MSVC build environment
|
||||||
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
with:
|
||||||
|
arch: x64
|
||||||
|
|
||||||
- name: Build installers
|
- name: Build installers
|
||||||
shell: bash
|
shell: cmd
|
||||||
run: bun run --filter gridline-desktop tauri build
|
run: |
|
||||||
|
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
bun run --filter gridline-desktop tauri build
|
||||||
|
|
||||||
- name: Create release and upload Windows assets
|
- name: Create release and upload Windows assets
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -227,13 +257,19 @@ jobs:
|
|||||||
AUTH="Authorization: token ${GITHUB_TOKEN}"
|
AUTH="Authorization: token ${GITHUB_TOKEN}"
|
||||||
|
|
||||||
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
if [ -z "$REL_ID" ]; then
|
if [ -z "$REL_ID" ]; then
|
||||||
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
BODY="Gridline ${TAG#v} - Linux and Windows installers."
|
||||||
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
||||||
"${API}/releases" \
|
"${API}/releases" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$REL_ID" ]; then
|
||||||
|
# A parallel job may have created the release between our GET and
|
||||||
|
# POST (create returns 422). Re-fetch by tag and upload to it.
|
||||||
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ name: Release
|
|||||||
# git checkout prod && git pull
|
# git checkout prod && git pull
|
||||||
# git tag v0.5.0 && git push origin v0.5.0
|
# git tag v0.5.0 && git push origin v0.5.0
|
||||||
#
|
#
|
||||||
# SIGNING STATUS: macOS bundles are AD-HOC signed via tauri.conf.json
|
# SIGNING STATUS: macOS is Developer-ID signed + notarized via
|
||||||
# (bundle.macOS.signingIdentity: "-", hardenedRuntime: false). That replaces
|
# scripts/release-mac.sh (built manually on Adrian's machine). NOTE: this
|
||||||
# the linker-only signature that made macOS report the app as "damaged" and
|
# GitHub workflow is NOT the active release path — Gitea Actions uses
|
||||||
# refuse Finder launches — users now get the standard "Open Anyway" prompt
|
# .gitea/workflows/release.yml (Linux/Windows only). Linux/Windows installers
|
||||||
# (or run `sudo xattr -dr com.apple.quarantine` for the damaged-error case, see
|
# remain unsigned.
|
||||||
# README). Still NOT Developer-ID signed / notarized (no certs yet).
|
|
||||||
# tauri-action automatically signs + notarizes when the signing secrets are
|
|
||||||
# present, so the moment we add APPLE_CERTIFICATE / APPLE_API_KEY /
|
|
||||||
# WINDOWS_CERTIFICATE (or Azure Trusted Signing) to repo secrets, future
|
|
||||||
# builds are properly signed — no changes to this file required.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
+10
@@ -38,3 +38,13 @@ docs/superpowers/
|
|||||||
.env
|
.env
|
||||||
!.env.example
|
!.env.example
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Bundled DB client tools (built/downloaded during release, never committed)
|
||||||
|
desktop/src-tauri/resources/pg_tools/pg_dump
|
||||||
|
desktop/src-tauri/resources/pg_tools/pg_restore
|
||||||
|
desktop/src-tauri/resources/pg_tools/psql
|
||||||
|
desktop/src-tauri/resources/pg_tools/libpq.5.dylib
|
||||||
|
desktop/src-tauri/resources/mysql_tools/mariadb
|
||||||
|
desktop/src-tauri/resources/mysql_tools/mariadb-dump
|
||||||
|
desktop/src-tauri/resources/mysql_tools/libcrypto.3.dylib
|
||||||
|
desktop/src-tauri/resources/mysql_tools/libssl.3.dylib
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ Cut a release by tagging the **`prod`** branch once the PR is merged — `git ta
|
|||||||
- `desktop/src/lib/version.test.ts` and `desktop/src/lib/docs-coverage.test.ts` if they assert the version
|
- `desktop/src/lib/version.test.ts` and `desktop/src/lib/docs-coverage.test.ts` if they assert the version
|
||||||
- **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.9/<file>`). tauri-action uses default versioned asset names (`Gridline_<ver>_aarch64.dmg`, `Gridline-<ver>-1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md).
|
- **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.9/<file>`). tauri-action uses default versioned asset names (`Gridline_<ver>_aarch64.dmg`, `Gridline-<ver>-1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md).
|
||||||
- **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step.
|
- **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step.
|
||||||
- **Release notes must include the macOS first-launch instructions** — until a Developer ID signing certificate is secured, releases are ad-hoc signed only (not notarized), so every macOS user hits a Gatekeeper prompt. The draft release body must include: right-click → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**) for "developer cannot be verified", and `sudo xattr -dr com.apple.quarantine /Applications/Gridline.app` (with the re-run-after-every-upgrade note) for "damaged and can't be opened".
|
- **macOS is Developer-ID signed + notarized** via `scripts/release-mac.sh` (built manually on Adrian's machine; not in CI). The script auto-detects the Developer ID identity from the login keychain, reads notarization credentials from `~/.config/gridline/notarize.env` (App Store Connect API key), and verifies codesign + stapled notarization before uploading. If the cert or credentials are missing it fails with setup instructions (see the README's "macOS signing & notarization" section).
|
||||||
|
|
||||||
### Adding a Tauri Command
|
### Adding a Tauri Command
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ Planned work is prioritized in the [Project Roadmap](./ROADMAP.md) (source of tr
|
|||||||
| DB Viewer: SQLite browse + query | ✅ | Full support via rusqlite |
|
| DB Viewer: SQLite browse + query | ✅ | Full support via rusqlite |
|
||||||
| DB Viewer: MySQL browse + query + edit | ✅ | Full viewer: connect (SSL + SSH tunnel), databases/tables/columns/FKs, query + pagination, inline cell editing + changes queue, DDL copy (`SHOW CREATE TABLE`), CSV/JSON import — added in v0.7.0. PK-only editing (no ctid equivalent); VARBINARY `information_schema` columns decoded correctly |
|
| DB Viewer: MySQL browse + query + edit | ✅ | Full viewer: connect (SSL + SSH tunnel), databases/tables/columns/FKs, query + pagination, inline cell editing + changes queue, DDL copy (`SHOW CREATE TABLE`), CSV/JSON import — added in v0.7.0. PK-only editing (no ctid equivalent); VARBINARY `information_schema` columns decoded correctly |
|
||||||
| DB Viewer: Redis browse | ❌ | Connection + test only; browsing gated off with a clean "not supported" state (v0.7.0) |
|
| DB Viewer: Redis browse | ❌ | Connection + test only; browsing gated off with a clean "not supported" state (v0.7.0) |
|
||||||
| Password storage in OS keychain | ✅ | macOS Keychain, Linux Secret Service, Windows Credential Manager |
|
| Password storage in OS keychain | ✅ | macOS Keychain, Linux Secret Service, Windows Credential Manager; macOS items get an explicit `SecAccess` ACL pinning them to the app's code-signature requirement on every save (`keychain_acl.rs`) — items survive re-signs/upgrades without keychain permission prompts |
|
||||||
| Enable keychain toggle | ✅ | Default ON (opt-out); OFF = don't persist the DB password (session-only, re-prompt on connect) + purge existing keychain entry; SSH secrets stay keychain-only (v0.7.6) |
|
| Enable keychain toggle | ✅ | Default ON (opt-out); OFF = don't persist the DB password (session-only, re-prompt on connect) + purge existing keychain entry; SSH secrets stay keychain-only (v0.7.6) |
|
||||||
| SSH tunnel config UI | ✅ | Host, port, user, auth method, key path, passphrase fields |
|
| SSH tunnel config UI | ✅ | Host, port, user, auth method, key path, passphrase fields |
|
||||||
| SSH tunnel runtime | ✅ | Real ssh2 tunnel (password + key auth), binds 127.0.0.1 only, secrets in OS keychain (`ssh_password:<id>` / `ssh_passphrase:<id>`), closed on pool eviction / app exit; TLS downgraded to `require` through the tunnel |
|
| SSH tunnel runtime | ✅ | Real ssh2 tunnel (password + key auth), binds 127.0.0.1 only, secrets in OS keychain (`ssh_password:<id>` / `ssh_passphrase:<id>`), closed on pool eviction / app exit; TLS downgraded to `require` through the tunnel |
|
||||||
|
|||||||
@@ -18,43 +18,46 @@
|
|||||||
<a href="https://react.dev/"><img src="https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB" alt="React" /></a>
|
<a href="https://react.dev/"><img src="https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB" alt="React" /></a>
|
||||||
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-D22128.svg?style=for-the-badge" alt="Apache 2.0 License" /></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-D22128.svg?style=for-the-badge" alt="Apache 2.0 License" /></a>
|
||||||
<a href="https://github.com/adrianbonpin/gridline/stargazers"><img src="https://img.shields.io/github/stars/adrianbonpin/gridline?style=for-the-badge&logo=github&label=Stars" alt="GitHub stars" /></a>
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline"><img src="https://img.shields.io/badge/Open_Source-Gitea-4183C4.svg?style=for-the-badge" alt="Open source on Gitea" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/adrianbonpin/gridline/releases"><img src="https://img.shields.io/badge/Download_Latest_Release-2ea44f?style=for-the-badge&logo=github&logoColor=white" alt="Download Latest" /></a>
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline/releases"><img src="https://img.shields.io/badge/Download_Latest_Release-2ea44f?style=for-the-badge" alt="Download Latest" /></a>
|
||||||
<a href="https://github.com/adrianbonpin/gridline/issues/new"><img src="https://img.shields.io/badge/Open_an_Issue-%23E4405F.svg?style=for-the-badge&logo=github&logoColor=white" alt="Open an Issue" /></a>
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline/issues/new"><img src="https://img.shields.io/badge/Open_an_Issue-%23E4405F.svg?style=for-the-badge" alt="Open an Issue" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.11**):
|
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.14**):
|
||||||
|
|
||||||
| OS | Architecture | Download |
|
| OS | Architecture | Download |
|
||||||
| :--------------------------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
| :--------------------------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.11_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_aarch64.dmg) |
|
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.14_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_aarch64.dmg) |
|
||||||
| **macOS** | Intel | [Gridline_0.7.11_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64.dmg) |
|
| **macOS** | Intel | [Gridline_0.7.14_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64.dmg) |
|
||||||
| **Windows** | x64 | [Gridline_0.7.11_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64-setup.exe) |
|
| **Windows** | x64 | [Gridline_0.7.14_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64-setup.exe) |
|
||||||
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.11_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.deb) |
|
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.14_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.deb) |
|
||||||
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.11-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline-0.7.11-1.x86_64.rpm) |
|
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.14-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline-0.7.14-1.x86_64.rpm) |
|
||||||
| **Other Linux** | amd64 | [Gridline_0.7.11_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.AppImage) |
|
| **Other Linux** | amd64 | [Gridline_0.7.14_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.AppImage) |
|
||||||
|
|
||||||
**macOS via Homebrew** (recommended for Mac users):
|
**macOS via Homebrew** (recommended for Mac users):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install --cask AdrianBonpin/gridline/gridline
|
# one-time setup (the tap is hosted on Gitea, not GitHub):
|
||||||
# or, after a one-time `brew tap AdrianBonpin/gridline`:
|
brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git
|
||||||
|
brew trust adrianbonpin/gridline
|
||||||
|
|
||||||
|
# install / upgrade:
|
||||||
brew install --cask gridline
|
brew install --cask gridline
|
||||||
```
|
```
|
||||||
|
|
||||||
The cask strips the macOS quarantine flag automatically during install, so **no manual `xattr` step is needed** on the Homebrew path — the app just launches. (Direct-DMG downloads still need the [first-launch instructions](#installers-are-unsigned-for-now) below.)
|
The cask installs the app directly. The DMGs are Developer-ID signed and notarized, so macOS trusts them on first launch — no `xattr` or "Open Anyway" step needed.
|
||||||
|
|
||||||
> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
|
> **macOS first launch:** Gridline is Developer-ID signed and notarized, so macOS opens it without a Gatekeeper prompt. Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.11
|
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.14
|
||||||
release assets, not at a moving "latest" target. On every new release,
|
release assets, not at a moving "latest" target. On every new release,
|
||||||
update BOTH tables here (Download + Which file should I download?) to the
|
update BOTH tables here (Download + Which file should I download?) to the
|
||||||
new version's asset names, which are tauri-action's default naming:
|
new version's asset names, which are tauri-action's default naming:
|
||||||
@@ -99,6 +102,12 @@ Gridline is built for developers and small teams who manage multiple database en
|
|||||||
<details>
|
<details>
|
||||||
<summary>View the full changelog</summary>
|
<summary>View the full changelog</summary>
|
||||||
|
|
||||||
|
- **v0.7.14** — **No more repeated macOS keychain prompts**: saved connection passwords and SSH secrets are now ACL-pinned to the app's code signature on every save (`keychain_acl`), so app upgrades never re-trigger the macOS keychain access dialog (users upgrading from pre-signing builds see one final prompt — choose **Always Allow**). **Install via Homebrew**: `brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git` then `brew install --cask gridline`; version bump.
|
||||||
|
|
||||||
|
- **v0.7.13** — **macOS is now Developer-ID signed and notarized**: the app is properly code-signed with a Developer ID Application certificate and notarized by Apple, so macOS opens it without the Gatekeeper "Open Anyway" prompt or the `xattr` quarantine workaround. The release script (`scripts/release-mac.sh`) auto-detects the signing identity from the keychain, notarizes + staples both architectures, and verifies the result before uploading; version bump.
|
||||||
|
|
||||||
|
- **v0.7.12** — **Reliable cell editing for non-text columns**: editing a PostgreSQL cell whose type isn't `text` (integers, booleans, UUIDs, `json`/`jsonb`, timestamps, enums, numerics, …) previously failed with a Rust serialization error; edited values are now sent to PostgreSQL in text wire format so the server parses them into the column's own type. **Recent connections fix**: the Recent strip no longer renders empty on first launch when the connections list hadn't loaded yet. **Full CI release pipeline for Windows**: self-hosted Windows runner builds the `.exe`/`.msi` installers (bundled `pg_dump`/`pg_restore`/`psql` + MariaDB clients) and uploads them to the release alongside Linux; version bump.
|
||||||
|
|
||||||
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
|
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
|
||||||
|
|
||||||
- **v0.7.9** — **macOS install/launch fixes**: the app's local store now opens under the OS app-data directory (a cwd-relative `gridline.db` made Finder-launched copies silently exit with a Rust panic before the UI started); macOS bundles are **ad-hoc signed at build time** (`signingIdentity "-"`), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusals) — users now get a standard one-time Gatekeeper prompt (see the [install notes](#installers-are-unsigned-for-now), incl. the `xattr` workaround); **OpenSSL is vendored** into the `ssh2` build so the release no longer depends on a Homebrew OpenSSL path; version bump.
|
- **v0.7.9** — **macOS install/launch fixes**: the app's local store now opens under the OS app-data directory (a cwd-relative `gridline.db` made Finder-launched copies silently exit with a Rust panic before the UI started); macOS bundles are **ad-hoc signed at build time** (`signingIdentity "-"`), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusals) — users now get a standard one-time Gatekeeper prompt (see the [install notes](#installers-are-unsigned-for-now), incl. the `xattr` workaround); **OpenSSL is vendored** into the `ssh2` build so the release no longer depends on a Homebrew OpenSSL path; version bump.
|
||||||
@@ -301,41 +310,44 @@ _Notes: DB Pro is an Electron app (launched Nov 2025) whose marketing copy overc
|
|||||||
|
|
||||||
### Download a release
|
### Download a release
|
||||||
|
|
||||||
Pre-built installers for macOS, Windows, and Linux are published on the [Releases](https://github.com/adrianbonpin/gridline/releases) page.
|
Pre-built installers for macOS, Windows, and Linux are published on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page.
|
||||||
|
|
||||||
> ⚠️ Gridline is under active development. Expect rough edges and please [open issues](https://github.com/adrianbonpin/gridline/issues/new) when you hit them.
|
> ⚠️ Gridline is under active development. Expect rough edges and please [open issues](https://git.ranio.xyz/adrianbonpin/gridline/issues/new) when you hit them.
|
||||||
|
|
||||||
#### Installers are unsigned (for now)
|
#### macOS signing & notarization
|
||||||
|
|
||||||
Gridline is currently distributed **unsigned** — it doesn't pay for code-signing certificates yet (macOS builds are *ad-hoc signed*, so they pass Apple Silicon's launch checks but aren't Developer-ID signed or notarized). Your OS will warn you the first time you open it. This is expected — the app is safe, it just hasn't paid the signing fee:
|
Gridline's macOS builds are **Developer-ID signed and notarized**, so macOS opens them without a Gatekeeper prompt. This is configured on the machine that runs `scripts/release-mac.sh` (macOS is built manually, not in CI).
|
||||||
|
|
||||||
> **Homebrew users:** the [Homebrew cask](#download) strips the quarantine flag automatically during install, so you skip all of the steps below — the app just launches.
|
**One-time setup** (only the repo owner needs to do this):
|
||||||
|
|
||||||
- **macOS:** if you see *"Gridline can't be opened because the developer cannot be verified"* (or the app simply won't open from Finder), right-click the app → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**). Do this once per version.
|
1. **Create a Developer ID Application certificate** — [developer.apple.com](https://developer.apple.com) → Certificates, Identifiers & Profiles → **+** → **Developer ID Application**. Generate a Certificate Signing Request from Keychain Access (Certificate Assistant → *Request a Certificate From a Certificate Authority*), upload it, download the `.cer`, and double-click to install into your login keychain. Verify with `security find-identity -v -p codesigning`.
|
||||||
- **macOS — "Gridline is damaged and can't be opened":** remove the quarantine flag macOS attaches to downloaded apps, then launch normally:
|
2. **Create an App Store Connect API key** for notarization — [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → Users and Access → **Integrations** → **App Store Connect API** → **+** (name it, *Developer* access). Download the `.p8` (shown once) and note the **Key ID** and **Issuer ID**.
|
||||||
|
3. **Save the notarization credentials** to `~/.config/gridline/notarize.env` (never committed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo xattr -dr com.apple.quarantine /Applications/Gridline.app
|
APPLE_API_KEY=<Key ID>
|
||||||
|
APPLE_API_ISSUER=<Issuer ID>
|
||||||
|
APPLE_API_KEY_PATH=/absolute/path/to/AuthKey_<KeyID>.p8
|
||||||
```
|
```
|
||||||
|
|
||||||
(`sudo` is required — some files inside the bundle are read-only, so removing the flag needs admin rights.) Re-run after **every upgrade** — each freshly-downloaded copy gets re-quarantined.
|
(Alternatively, drop the `.p8` at `~/.appstoreconnect/private_keys/AuthKey_<KeyID>.p8` and omit `APPLE_API_KEY_PATH`.)
|
||||||
- **Windows:** on the SmartScreen prompt, click **More info** → **Run anyway**.
|
|
||||||
- **Linux:** no warning — install and run normally.
|
|
||||||
|
|
||||||
Code signing **will be added in the future** (Apple Developer Program + a Windows signing cert, e.g. Azure Trusted Signing) — the CI workflow is already wired to pick up the signing secrets automatically the moment they exist, no workflow changes needed.
|
`scripts/release-mac.sh` auto-detects the Developer ID identity from your keychain, signs both architectures, notarizes + staples them, and verifies the result before uploading. If the certificate or credentials are missing it fails with instructions (set `GRIDLINE_SKIP_NOTARIZE=1` to build signed-but-unnotarized for testing only).
|
||||||
|
|
||||||
|
**Windows** still shows a SmartScreen prompt (no Windows signing cert yet); **Linux** installs without a warning.
|
||||||
|
|
||||||
#### Which file should I download?
|
#### Which file should I download?
|
||||||
|
|
||||||
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.11**):
|
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.14**):
|
||||||
|
|
||||||
| Your system | Download this | Notes |
|
| Your system | Download this | Notes |
|
||||||
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
||||||
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.11_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_aarch64.dmg) | `aarch64` = Apple's own chip |
|
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.14_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_aarch64.dmg) | `aarch64` = Apple's own chip |
|
||||||
| macOS **Intel** | [Gridline_0.7.11_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64.dmg) | `x64` = Intel/AMD |
|
| macOS **Intel** | [Gridline_0.7.14_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64.dmg) | `x64` = Intel/AMD |
|
||||||
| **Windows** (most PCs) | [Gridline_0.7.11_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
| **Windows** (most PCs) | [Gridline_0.7.14_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
||||||
| **Debian / Ubuntu** | [Gridline_0.7.11_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.11_amd64.deb` |
|
| **Debian / Ubuntu** | [Gridline_0.7.14_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.14_amd64.deb` |
|
||||||
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.11-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline-0.7.11-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.11-1.x86_64.rpm` |
|
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.14-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline-0.7.14-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.14-1.x86_64.rpm` |
|
||||||
| **Any other Linux** | [Gridline_0.7.11_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
| **Any other Linux** | [Gridline_0.7.14_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.14/Gridline_0.7.14_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
||||||
|
|
||||||
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
|
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
|
||||||
|
|
||||||
@@ -345,8 +357,8 @@ Cutting a release is one command — CI builds everything. **Releases are cut fr
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout prod && git pull
|
git checkout prod && git pull
|
||||||
git tag v0.7.11
|
git tag v0.7.14
|
||||||
git push origin v0.7.11
|
git push origin v0.7.14
|
||||||
```
|
```
|
||||||
|
|
||||||
Gitea Actions (`.gitea/workflows/release.yml`) builds the **Linux and Windows** installers on the self-hosted runners, then opens a **draft release** on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page. **macOS is built manually** on the dev Mac and its DMGs uploaded to the same release via `scripts/release-mac.sh`. Review the draft and hit **Publish release**.
|
Gitea Actions (`.gitea/workflows/release.yml`) builds the **Linux and Windows** installers on the self-hosted runners, then opens a **draft release** on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page. **macOS is built manually** on the dev Mac and its DMGs uploaded to the same release via `scripts/release-mac.sh`. Review the draft and hit **Publish release**.
|
||||||
@@ -357,7 +369,7 @@ Before tagging, make sure the version number is in sync across `desktop/package.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone the repository
|
# 1. Clone the repository
|
||||||
git clone https://github.com/adrianbonpin/gridline.git
|
git clone https://git.ranio.xyz/adrianbonpin/gridline.git
|
||||||
cd gridline
|
cd gridline
|
||||||
|
|
||||||
# 2. Install all workspace dependencies
|
# 2. Install all workspace dependencies
|
||||||
@@ -473,7 +485,7 @@ Highlights of what's next:
|
|||||||
|
|
||||||
Contributions, bug reports, and feature ideas are welcome. Gridline is Apache 2.0-licensed and intentionally stays open — no paywalled tiers, no bundled proprietary services.
|
Contributions, bug reports, and feature ideas are welcome. Gridline is Apache 2.0-licensed and intentionally stays open — no paywalled tiers, no bundled proprietary services.
|
||||||
|
|
||||||
- Open an issue via the **[template chooser](https://github.com/AdrianBonpin/gridline/issues/new)** — pick **Bug report** (with an environment table so we can reproduce issues quickly), **Feature request**, or **Improvement** (UX polish for existing features). New issues are auto-assigned to the maintainer.
|
- Open an issue via the **[template chooser](https://git.ranio.xyz/adrianbonpin/gridline/issues/new)** — pick **Bug report** (with an environment table so we can reproduce issues quickly), **Feature request**, or **Improvement** (UX polish for existing features). New issues are auto-assigned to the maintainer.
|
||||||
- Submit a pull request. Keep Tauri commands thin, type IPC boundaries explicitly, and follow the existing Rust/React conventions.
|
- Submit a pull request. Keep Tauri commands thin, type IPC boundaries explicitly, and follow the existing Rust/React conventions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gridline-desktop",
|
"name": "gridline-desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.11",
|
"version": "0.7.14",
|
||||||
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+5
-1
@@ -1783,9 +1783,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.10"
|
version = "0.7.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"core-foundation 0.10.1",
|
||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"redis",
|
"redis",
|
||||||
@@ -1794,6 +1796,8 @@ dependencies = [
|
|||||||
"rustls-native-certs",
|
"rustls-native-certs",
|
||||||
"rustls-pemfile 2.2.0",
|
"rustls-pemfile 2.2.0",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
"security-framework 3.7.0",
|
||||||
|
"security-framework-sys",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.11"
|
version = "0.7.14"
|
||||||
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -31,6 +31,7 @@ tauri-plugin-dialog = "2"
|
|||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||||
|
bytes = "1"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
||||||
redis = { version = "0.27", features = ["tokio-comp"] }
|
redis = { version = "0.27", features = ["tokio-comp"] }
|
||||||
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
||||||
@@ -48,3 +49,12 @@ rustls-pemfile = "2"
|
|||||||
rustls-pki-types = "1"
|
rustls-pki-types = "1"
|
||||||
rustls-native-certs = "0.7"
|
rustls-native-certs = "0.7"
|
||||||
|
|
||||||
|
# Keychain ACL normalization (explicit SecAccess on stored secrets so items
|
||||||
|
# survive app re-signs without permission prompts) — macOS only.
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
security-framework = "3.7"
|
||||||
|
security-framework-sys = "2.17"
|
||||||
|
# core-foundation: CFString/CFArray/TCFType helpers for the SecAccess FFI
|
||||||
|
# (already a transitive dep of security-framework; pinned to the same 0.10 line)
|
||||||
|
core-foundation = "0.10"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- Required for the WKWebView (JIT-compiled JavaScript). Without this the
|
||||||
|
app crashes under the hardened runtime. -->
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<!-- The WebView allocates executable memory for JIT; allow it. -->
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<!-- Tauri's dev server / debug builds rely on DYLD_* environment variables. -->
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -8,10 +8,12 @@ use crate::models::db_viewer::{
|
|||||||
Change, ColumnInfo, ConstraintInfo, EnumInfo, ExtensionInfo, FunctionInfo, IndexInfo,
|
Change, ColumnInfo, ConstraintInfo, EnumInfo, ExtensionInfo, FunctionInfo, IndexInfo,
|
||||||
QueryResult, SequenceInfo, TableInfo, TriggerInfo,
|
QueryResult, SequenceInfo, TableInfo, TriggerInfo,
|
||||||
};
|
};
|
||||||
|
use bytes::BytesMut;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio_postgres::types::ToSql;
|
use tokio_postgres::types::{Format, IsNull, ToSql, Type};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper functions
|
// Helper functions
|
||||||
@@ -585,26 +587,75 @@ pub fn build_pg_insert_sql(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A value bound to a PostgreSQL parameter in TEXT wire format, accepting any
|
||||||
|
/// parameter type.
|
||||||
|
///
|
||||||
|
/// The frontend ships edited values as plain strings (and PK locators as
|
||||||
|
/// numbers/booleans), but binding a native `String` fails for every non-text
|
||||||
|
/// column type — `String::accepts` only matches `varchar`/`text`/`bpchar`/
|
||||||
|
/// `name` — with `error serializing parameter N: cannot convert between the
|
||||||
|
/// Rust type ... and the Postgres type ...`. Binding a native `i64` is also
|
||||||
|
/// wrong because `i64::accepts` only matches `int8`.
|
||||||
|
///
|
||||||
|
/// `PgText` accepts *any* parameter type and writes the value as UTF-8 text in
|
||||||
|
/// text wire format, so PostgreSQL itself parses the literal into the target
|
||||||
|
/// column type. This works uniformly for `integer`, `bigint`, `numeric`,
|
||||||
|
/// `boolean`, `uuid`, `json`/`jsonb`, timestamps, enums, etc. — exactly what
|
||||||
|
/// PostgreSQL does for an `'<literal>'` in a query.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PgText(String);
|
||||||
|
|
||||||
|
impl From<&str> for PgText {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
PgText(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for PgText {
|
||||||
|
fn to_sql(
|
||||||
|
&self,
|
||||||
|
_ty: &Type,
|
||||||
|
out: &mut BytesMut,
|
||||||
|
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
|
||||||
|
out.extend_from_slice(self.0.as_bytes());
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepts(_ty: &Type) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_format(&self, _ty: &Type) -> Format {
|
||||||
|
Format::Text
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_sql_checked(
|
||||||
|
&self,
|
||||||
|
ty: &Type,
|
||||||
|
out: &mut BytesMut,
|
||||||
|
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
|
||||||
|
self.to_sql(ty, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a JSON value into a boxed PostgreSQL-bindable value.
|
/// Convert a JSON value into a boxed PostgreSQL-bindable value.
|
||||||
///
|
///
|
||||||
/// Maps common JSON types to `tokio_postgres::types::ToSql` implementors.
|
/// Every non-null value is bound as a `PgText` (text wire format) so the server
|
||||||
/// Unknown/complex types are stringified as a fallback.
|
/// parses it into the target column type; SQL `NULL` is bound as `None`. This
|
||||||
|
/// is deliberately type-agnostic — the caller already knows the column type
|
||||||
|
/// through the SQL context (e.g. `SET "col" = $1` lets PostgreSQL infer `$1`
|
||||||
|
/// as `col`'s type).
|
||||||
fn pg_box_value(v: &serde_json::Value) -> Box<dyn ToSql + Send + Sync> {
|
fn pg_box_value(v: &serde_json::Value) -> Box<dyn ToSql + Send + Sync> {
|
||||||
match v {
|
if v.is_null() {
|
||||||
serde_json::Value::Null => Box::new(Option::<String>::None),
|
return Box::new(Option::<PgText>::None);
|
||||||
serde_json::Value::Bool(b) => Box::new(*b),
|
|
||||||
serde_json::Value::Number(n) => {
|
|
||||||
if let Some(i) = n.as_i64() {
|
|
||||||
Box::new(i)
|
|
||||||
} else if let Some(f) = n.as_f64() {
|
|
||||||
Box::new(f)
|
|
||||||
} else {
|
|
||||||
Box::new(n.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::Value::String(s) => Box::new(s.clone()),
|
|
||||||
other => Box::new(other.to_string()),
|
|
||||||
}
|
}
|
||||||
|
let text = match v {
|
||||||
|
serde_json::Value::String(s) => s.clone(),
|
||||||
|
serde_json::Value::Bool(b) => b.to_string(),
|
||||||
|
serde_json::Value::Number(n) => n.to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
Box::new(PgText::from(text.as_str()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a JSON value into a `rusqlite::types::Value` for SQLite binding.
|
/// Convert a JSON value into a `rusqlite::types::Value` for SQLite binding.
|
||||||
@@ -3320,6 +3371,78 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// PgText: universal text-format parameter binding (serialization fix)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_accepts_any_postgres_type() {
|
||||||
|
// String::accepts only matches text-ish types, so a plain String can't
|
||||||
|
// bind to an integer/boolean/uuid/jsonb column. PgText must accept all
|
||||||
|
// of these (the server parses the text into the column's own type).
|
||||||
|
for ty in [
|
||||||
|
Type::TEXT,
|
||||||
|
Type::VARCHAR,
|
||||||
|
Type::INT4,
|
||||||
|
Type::INT8,
|
||||||
|
Type::FLOAT8,
|
||||||
|
Type::NUMERIC,
|
||||||
|
Type::BOOL,
|
||||||
|
Type::UUID,
|
||||||
|
Type::JSON,
|
||||||
|
Type::JSONB,
|
||||||
|
Type::TIMESTAMP,
|
||||||
|
Type::TIMESTAMPTZ,
|
||||||
|
Type::DATE,
|
||||||
|
Type::TIME,
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
PgText::accepts(&ty),
|
||||||
|
"PgText must accept {:?}",
|
||||||
|
ty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sanity: a native String must NOT accept a non-text type (proving the
|
||||||
|
// root cause this fix addresses).
|
||||||
|
assert!(!<&str as ToSql>::accepts(&Type::INT4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_serializes_as_utf8_text() {
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use tokio_postgres::types::IsNull;
|
||||||
|
|
||||||
|
let val = pg_box_value(&serde_json::json!("42"));
|
||||||
|
let mut out = BytesMut::new();
|
||||||
|
let isnull = val.to_sql_checked(&Type::INT4, &mut out).unwrap();
|
||||||
|
assert!(matches!(isnull, IsNull::No));
|
||||||
|
assert_eq!(&out[..], b"42");
|
||||||
|
// Text wire format: the bytes are raw UTF-8, not binary-encoded int.
|
||||||
|
assert!(matches!(val.encode_format(&Type::INT4), Format::Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_boxes_null_to_sql_null() {
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use tokio_postgres::types::IsNull;
|
||||||
|
|
||||||
|
let val = pg_box_value(&serde_json::Value::Null);
|
||||||
|
let mut out = BytesMut::new();
|
||||||
|
assert!(matches!(
|
||||||
|
val.to_sql_checked(&Type::TEXT, &mut out).unwrap(),
|
||||||
|
IsNull::Yes
|
||||||
|
));
|
||||||
|
assert!(out.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_stringifies_booleans_and_numbers() {
|
||||||
|
let b = pg_box_value(&serde_json::json!(true));
|
||||||
|
assert!(matches!(b.encode_format(&Type::BOOL), Format::Text));
|
||||||
|
let n = pg_box_value(&serde_json::json!(5));
|
||||||
|
assert!(matches!(n.encode_format(&Type::INT4), Format::Text));
|
||||||
|
}
|
||||||
|
|
||||||
async fn fresh_pool_manager() -> tokio::sync::Mutex<ConnectionPoolManager> {
|
async fn fresh_pool_manager() -> tokio::sync::Mutex<ConnectionPoolManager> {
|
||||||
tokio::sync::Mutex::new(ConnectionPoolManager::new())
|
tokio::sync::Mutex::new(ConnectionPoolManager::new())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::keychain_acl;
|
||||||
use tauri_plugin_keyring_store::KeyringExt;
|
use tauri_plugin_keyring_store::KeyringExt;
|
||||||
|
|
||||||
/// Store a connection password in the OS keychain.
|
/// Store a connection password in the OS keychain.
|
||||||
@@ -11,7 +12,10 @@ pub fn save_connection_password(
|
|||||||
app.keyring()
|
app.keyring()
|
||||||
.store
|
.store
|
||||||
.set_password(&connection_id, &password)
|
.set_password(&connection_id, &password)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())?;
|
||||||
|
// Best-effort: pin the item's ACL to this app so re-signed builds never prompt.
|
||||||
|
keychain_acl::after_store(&app, &connection_id);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve a connection password from the OS keychain.
|
/// Retrieve a connection password from the OS keychain.
|
||||||
@@ -55,7 +59,9 @@ pub fn save_connection_ssh_password(
|
|||||||
app.keyring()
|
app.keyring()
|
||||||
.store
|
.store
|
||||||
.set_password(&ssh_account("password", &connection_id), &password)
|
.set_password(&ssh_account("password", &connection_id), &password)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())?;
|
||||||
|
keychain_acl::after_store(&app, &ssh_account("password", &connection_id));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve an SSH tunnel password from the OS keychain.
|
/// Retrieve an SSH tunnel password from the OS keychain.
|
||||||
@@ -93,7 +99,9 @@ pub fn save_connection_ssh_passphrase(
|
|||||||
app.keyring()
|
app.keyring()
|
||||||
.store
|
.store
|
||||||
.set_password(&ssh_account("passphrase", &connection_id), &passphrase)
|
.set_password(&ssh_account("passphrase", &connection_id), &passphrase)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())?;
|
||||||
|
keychain_acl::after_store(&app, &ssh_account("passphrase", &connection_id));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve an SSH private-key passphrase from the OS keychain.
|
/// Retrieve an SSH private-key passphrase from the OS keychain.
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
//! Keychain ACL normalization for stored secrets (macOS).
|
||||||
|
//!
|
||||||
|
//! # Why this exists
|
||||||
|
//!
|
||||||
|
//! Generic-password items stored via the legacy keychain API get a default ACL
|
||||||
|
//! that grants access to "the application that created the item". When a Tauri
|
||||||
|
//! app is rebuilt, its code signature changes (dev builds are ad-hoc signed, so
|
||||||
|
//! the identity differs on every build), and the new binary no longer matches
|
||||||
|
//! the item's ACL — macOS then shows the "Gridline wants to access your
|
||||||
|
//! keychain" prompt.
|
||||||
|
//!
|
||||||
|
//! The fix: after storing a secret, replace the item's ACL with an explicit
|
||||||
|
//! `SecAccess` whose trusted-application list contains exactly *this app*,
|
||||||
|
//! resolved by `SecTrustedApplicationCreateFromPath(NULL)` — the current
|
||||||
|
//! process's code-signature requirement. For a Developer ID-signed release
|
||||||
|
//! build that requirement is stable across versions (anchored to the signing
|
||||||
|
//! certificate), so items stay accessible without prompts through upgrades.
|
||||||
|
//!
|
||||||
|
//! # Prompt behavior
|
||||||
|
//!
|
||||||
|
//! - Item created by this build → the ACL set is silent.
|
||||||
|
//! - Item created by an older/differently-signed build → macOS shows the
|
||||||
|
//! standard one-time authorization dialog; "Always Allow" replaces the ACL
|
||||||
|
//! and the prompt never reappears (the same dialog the app would show on the
|
||||||
|
//! first read anyway — this just makes the result permanent and clean).
|
||||||
|
//!
|
||||||
|
//! All operations are best-effort: `after_store` swallows errors so keychain
|
||||||
|
//! writes never fail because of ACL normalization.
|
||||||
|
|
||||||
|
/// Best-effort ACL normalization after a successful keychain write.
|
||||||
|
///
|
||||||
|
/// Resolves the service name the same way `tauri-plugin-keyring-store` does
|
||||||
|
/// (the Tauri bundle identifier) and never fails the caller's operation.
|
||||||
|
pub fn after_store(app: &tauri::AppHandle, account: &str) {
|
||||||
|
let service = app.config().identifier.clone();
|
||||||
|
if let Err(e) = ensure_item_acl(&service, account) {
|
||||||
|
eprintln!("[keychain_acl] skipped for {account}: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grant this app (by its code-signature requirement) access to a keychain item.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn ensure_item_acl(service: &str, account: &str) -> Result<(), String> {
|
||||||
|
use core_foundation::array::CFArray;
|
||||||
|
use core_foundation::base::{CFTypeRef, TCFType};
|
||||||
|
use core_foundation::string::CFString;
|
||||||
|
use security_framework::item::{ItemClass, ItemSearchOptions, Reference, SearchResult};
|
||||||
|
use security_framework::os::macos::access::SecAccess;
|
||||||
|
use security_framework_sys::base::SecAccessRef;
|
||||||
|
|
||||||
|
// Locate the item WITHOUT reading its secret: a ref-only lookup
|
||||||
|
// (`kSecReturnRef`, no data/attributes) never triggers an authorization
|
||||||
|
// prompt, so merely probing an item we can't yet access is safe.
|
||||||
|
let found = ItemSearchOptions::new()
|
||||||
|
.class(ItemClass::generic_password())
|
||||||
|
.service(service)
|
||||||
|
.account(account)
|
||||||
|
.load_refs(true)
|
||||||
|
.load_data(false)
|
||||||
|
.load_attributes(false)
|
||||||
|
.search()
|
||||||
|
.map_err(|e| format!("keychain item lookup failed: {e}"))?;
|
||||||
|
|
||||||
|
let item = match found.first() {
|
||||||
|
Some(SearchResult::Ref(Reference::KeychainItem(item))) => item,
|
||||||
|
// No item (yet) or an unexpected result shape — nothing to normalize.
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
fn cvt(status: i32) -> Result<(), String> {
|
||||||
|
if status == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("security framework error {status}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build an access object whose trusted list is exactly this app. A NULL
|
||||||
|
// path resolves the trusted application to the current process's code
|
||||||
|
// signature requirement.
|
||||||
|
unsafe {
|
||||||
|
let mut trusted_app_ref: *mut OpaqueSecTrustedApplication = std::ptr::null_mut();
|
||||||
|
cvt(SecTrustedApplicationCreateFromPath(std::ptr::null(), &mut trusted_app_ref))?;
|
||||||
|
let trusted_app = SecTrustedApplication::wrap_under_create_rule(trusted_app_ref);
|
||||||
|
let trusted_list = CFArray::from_CFTypes(&[trusted_app]);
|
||||||
|
|
||||||
|
let descriptor = CFString::new("Gridline");
|
||||||
|
let mut access_ref: SecAccessRef = std::ptr::null_mut();
|
||||||
|
cvt(SecAccessCreate(
|
||||||
|
descriptor.as_concrete_TypeRef() as CFTypeRef,
|
||||||
|
trusted_list.as_concrete_TypeRef() as CFTypeRef,
|
||||||
|
&mut access_ref,
|
||||||
|
))?;
|
||||||
|
let access = SecAccess::wrap_under_create_rule(access_ref);
|
||||||
|
|
||||||
|
// Replace the item's ACL. Silent when we already own the item;
|
||||||
|
// one-time authorization dialog when the item was created by a
|
||||||
|
// differently-signed build.
|
||||||
|
cvt(SecKeychainItemSetAccess(
|
||||||
|
item.as_concrete_TypeRef(),
|
||||||
|
access.as_concrete_TypeRef(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-macOS platforms have no keychain ACL concept — nothing to do.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn ensure_item_acl(_service: &str, _account: &str) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos_ffi {
|
||||||
|
use core_foundation::base::{CFTypeRef, CFTypeID};
|
||||||
|
|
||||||
|
/// Minimal `TCFType` wrapper for `SecTrustedApplication` — the
|
||||||
|
/// security-framework crate exposes no high-level API for it.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct OpaqueSecTrustedApplication {
|
||||||
|
_priv: [u8; 0],
|
||||||
|
}
|
||||||
|
pub type SecTrustedApplicationRef = *mut OpaqueSecTrustedApplication;
|
||||||
|
|
||||||
|
use core_foundation::{declare_TCFType, impl_TCFType};
|
||||||
|
declare_TCFType!(SecTrustedApplication, SecTrustedApplicationRef);
|
||||||
|
impl_TCFType!(
|
||||||
|
SecTrustedApplication,
|
||||||
|
SecTrustedApplicationRef,
|
||||||
|
SecTrustedApplicationGetTypeID
|
||||||
|
);
|
||||||
|
unsafe impl Send for SecTrustedApplication {}
|
||||||
|
unsafe impl Sync for SecTrustedApplication {}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
// SecAccess/SecTrustedApplication APIs are deprecated in the macOS SDK
|
||||||
|
// but remain fully functional (the legacy keychain store this app uses
|
||||||
|
// via tauri-plugin-keyring-store relies on the same API family).
|
||||||
|
pub fn SecTrustedApplicationGetTypeID() -> CFTypeID;
|
||||||
|
pub fn SecTrustedApplicationCreateFromPath(
|
||||||
|
path: CFTypeRef,
|
||||||
|
app: *mut SecTrustedApplicationRef,
|
||||||
|
) -> i32;
|
||||||
|
pub fn SecAccessCreate(
|
||||||
|
descriptor: CFTypeRef,
|
||||||
|
trustedlist: CFTypeRef,
|
||||||
|
access: *mut security_framework_sys::base::SecAccessRef,
|
||||||
|
) -> i32;
|
||||||
|
pub fn SecKeychainItemSetAccess(
|
||||||
|
item: security_framework_sys::base::SecKeychainItemRef,
|
||||||
|
access: security_framework_sys::base::SecAccessRef,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use macos_ffi::{
|
||||||
|
OpaqueSecTrustedApplication, SecAccessCreate, SecKeychainItemSetAccess, SecTrustedApplication,
|
||||||
|
SecTrustedApplicationCreateFromPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(all(test, target_os = "macos"))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use security_framework::os::macos::keychain::SecKeychain;
|
||||||
|
use security_framework::os::macos::passwords::find_generic_password;
|
||||||
|
|
||||||
|
/// Real-keychain integration test: creates a generic-password item the
|
||||||
|
/// same way the keyring store does, normalizes its ACL, verifies the
|
||||||
|
/// secret is still readable, and cleans up.
|
||||||
|
///
|
||||||
|
/// Ignored by default (`cargo test`) so CI/dev runs never touch the login
|
||||||
|
/// keychain. Run explicitly with:
|
||||||
|
/// `cargo test -- --ignored keychain_acl`
|
||||||
|
#[test]
|
||||||
|
#[ignore = "touches the real login keychain"]
|
||||||
|
fn keychain_acl_normalizes_item() {
|
||||||
|
let keychain = SecKeychain::default().expect("default keychain");
|
||||||
|
let service = "com.adrianbonpin.gridline.acl-test";
|
||||||
|
let account = format!("acl-test-{}", std::process::id());
|
||||||
|
|
||||||
|
// Create the item exactly like apple-native-keyring-store does
|
||||||
|
// (legacy SecKeychain generic-password API).
|
||||||
|
keychain
|
||||||
|
.set_generic_password(service, &account, b"test-secret")
|
||||||
|
.expect("create test item");
|
||||||
|
|
||||||
|
// ACL normalization must succeed on an item we just created.
|
||||||
|
ensure_item_acl(service, &account).expect("normalize ACL");
|
||||||
|
|
||||||
|
// The item must remain readable with the correct secret.
|
||||||
|
let (password, item) =
|
||||||
|
find_generic_password(Some(&[keychain]), service, &account).expect("read back");
|
||||||
|
assert_eq!(password.as_ref(), b"test-secret");
|
||||||
|
|
||||||
|
// Cleanup: delete the test item.
|
||||||
|
item.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
mod cancel;
|
mod cancel;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod keychain_acl;
|
||||||
mod models;
|
mod models;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Gridline",
|
"productName": "Gridline",
|
||||||
"version": "0.7.11",
|
"version": "0.7.14",
|
||||||
"identifier": "com.adrianbonpin.gridline",
|
"identifier": "com.adrianbonpin.gridline",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
"resources": ["resources/pg_tools/*", "resources/mysql_tools/*"],
|
"resources": ["resources/pg_tools/*", "resources/mysql_tools/*"],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"signingIdentity": "-",
|
"signingIdentity": "-",
|
||||||
"hardenedRuntime": false
|
"hardenedRuntime": true,
|
||||||
|
"entitlements": "entitlements.plist"
|
||||||
},
|
},
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||||
|
|
||||||
describe("tauri bundle config (v0.7.11)", () => {
|
describe("tauri bundle config (v0.7.14)", () => {
|
||||||
it("declares bundled pg_tools resources", () => {
|
it("declares bundled pg_tools resources", () => {
|
||||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||||
});
|
});
|
||||||
it("version is 0.7.11", () => {
|
it("version is 0.7.14", () => {
|
||||||
expect(tauriConf.version).toBe("0.7.11");
|
expect(tauriConf.version).toBe("0.7.14");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import agents from "../../../AGENTS.md?raw";
|
import agents from "../../../AGENTS.md?raw";
|
||||||
import readme from "../../../README.md?raw";
|
import readme from "../../../README.md?raw";
|
||||||
|
|
||||||
describe("v0.7.11 docs coverage", () => {
|
describe("v0.7.14 docs coverage", () => {
|
||||||
it("AGENTS.md marks inline cell editing complete", () => {
|
it("AGENTS.md marks inline cell editing complete", () => {
|
||||||
expect(agents).toContain("Inline cell editing");
|
expect(agents).toContain("Inline cell editing");
|
||||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||||
@@ -24,8 +24,8 @@ describe("v0.7.11 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||||
});
|
});
|
||||||
it("README declares v0.7.11", () => {
|
it("README declares v0.7.14", () => {
|
||||||
expect(readme).toContain("0.7.11");
|
expect(readme).toContain("0.7.14");
|
||||||
});
|
});
|
||||||
it("AGENTS.md marks schema CRUD complete", () => {
|
it("AGENTS.md marks schema CRUD complete", () => {
|
||||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||||
@@ -64,9 +64,9 @@ describe("v0.7.11 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
||||||
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
||||||
});
|
});
|
||||||
it("README links to v0.7.11 assets in both download tables", () => {
|
it("README links to v0.7.14 assets in both download tables", () => {
|
||||||
expect(readme).toContain("releases/download/v0.7.11/");
|
expect(readme).toContain("releases/download/v0.7.14/");
|
||||||
expect(readme).toContain("Gridline_0.7.11_aarch64.dmg");
|
expect(readme).toContain("Gridline_0.7.14_aarch64.dmg");
|
||||||
expect(readme).toContain("Gridline-0.7.11-1.x86_64.rpm");
|
expect(readme).toContain("Gridline-0.7.14-1.x86_64.rpm");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
|
|
||||||
describe("version", () => {
|
describe("version", () => {
|
||||||
it("declares v0.7.11 across the app shell", () => {
|
it("declares v0.7.14 across the app shell", () => {
|
||||||
expect(pkg.version).toBe("0.7.11");
|
expect(pkg.version).toBe("0.7.14");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -225,6 +225,33 @@ describe("favorites / recents / move-selection", () => {
|
|||||||
expect(commands.recordRecentConnection).toHaveBeenCalledWith("c1");
|
expect(commands.recordRecentConnection).toHaveBeenCalledWith("c1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loadAll refreshes recents once connections are known (startup race fix)", async () => {
|
||||||
|
const conn = makeConn({ id: "c1", name: "Prod" });
|
||||||
|
vi.spyOn(commands, "getConnections").mockResolvedValue([conn]);
|
||||||
|
vi.spyOn(commands, "getFolders").mockResolvedValue([]);
|
||||||
|
vi.spyOn(commands, "getTags").mockResolvedValue([]);
|
||||||
|
vi.spyOn(commands, "getRecentConnections").mockResolvedValue([
|
||||||
|
{ connection_id: "c1", opened_at: "2026-08-02T00:00:00Z" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simulate the app-shell mount: loadAll fires and populates connections.
|
||||||
|
await useConnectionStore.getState().loadAll();
|
||||||
|
|
||||||
|
// The recent strip must now show the connection that was opened previously.
|
||||||
|
expect(commands.getRecentConnections).toHaveBeenCalledWith(8);
|
||||||
|
expect(useConnectionStore.getState().recent).toEqual([conn]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loadRecent maps cached recent ids against the current connection list", async () => {
|
||||||
|
const conn = makeConn({ id: "c1", name: "Prod" });
|
||||||
|
useConnectionStore.setState({ connections: [conn] });
|
||||||
|
vi.spyOn(commands, "getRecentConnections").mockResolvedValue([
|
||||||
|
{ connection_id: "c1", opened_at: "2026-08-02T00:00:00Z" },
|
||||||
|
]);
|
||||||
|
await useConnectionStore.getState().loadRecent();
|
||||||
|
expect(useConnectionStore.getState().recent).toEqual([conn]);
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplicateConnection", () => {
|
describe("duplicateConnection", () => {
|
||||||
it("copies fields with a '(copy)' name and favorite false", async () => {
|
it("copies fields with a '(copy)' name and favorite false", async () => {
|
||||||
const src = makeConn({ id: "c1", name: "Prod", folder_id: "f1", tag_ids: ["t1"], favorite: true });
|
const src = makeConn({ id: "c1", name: "Prod", folder_id: "f1", tag_ids: ["t1"], favorite: true });
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ export const useConnectionStore = create<ConnectionState>((set, get) => ({
|
|||||||
set((s) => ({ connections: [...s.connections].sort((a, b) => Number(b.favorite) - Number(a.favorite)) }));
|
set((s) => ({ connections: [...s.connections].sort((a, b) => Number(b.favorite) - Number(a.favorite)) }));
|
||||||
// Also load tag order
|
// Also load tag order
|
||||||
get().loadTagOrder();
|
get().loadTagOrder();
|
||||||
|
// Refresh recents once connections are known so the recent strip is not
|
||||||
|
// computed against an empty list (avoids a startup race with loadRecent).
|
||||||
|
get().loadRecent().catch(() => {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set({ loading: false, error: e instanceof Error ? e.message : String(e) });
|
set({ loading: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,8 @@
|
|||||||
#
|
#
|
||||||
# brew install --cask gridline
|
# brew install --cask gridline
|
||||||
#
|
#
|
||||||
# QUARANTINE: Gridline is ad-hoc signed (not Developer-ID signed/notarized),
|
# SIGNING: Gridline is Developer-ID signed + notarized, so macOS trusts it
|
||||||
# so Homebrew casks (which deliberately set com.apple.quarantine on the
|
# and no quarantine-stripping postflight is needed.
|
||||||
# downloaded artifact) would make Gatekeeper refuse to launch it. The
|
|
||||||
# `postflight` block strips the quarantine flag automatically — the standard
|
|
||||||
# GoReleaser-documented pattern for unsigned casks — so `brew install` works
|
|
||||||
# with no manual `xattr` step. This is a Gatekeeper bypass; drop the
|
|
||||||
# postflight once the app is properly signed + notarized.
|
|
||||||
|
|
||||||
cask "gridline" do
|
cask "gridline" do
|
||||||
arch arm: "aarch64", intel: "x64"
|
arch arm: "aarch64", intel: "x64"
|
||||||
@@ -26,24 +21,15 @@ cask "gridline" do
|
|||||||
sha256 arm: "@ARM_SHA256@",
|
sha256 arm: "@ARM_SHA256@",
|
||||||
intel: "@INTEL_SHA256@"
|
intel: "@INTEL_SHA256@"
|
||||||
|
|
||||||
url "https://github.com/AdrianBonpin/gridline/releases/download/v#{version}/Gridline_#{version}_#{arch}.dmg"
|
url "https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v#{version}/Gridline_#{version}_#{arch}.dmg"
|
||||||
name "Gridline"
|
name "Gridline"
|
||||||
desc "Open-source, cross-platform database GUI client for PostgreSQL"
|
desc "Open-source, cross-platform database GUI client for PostgreSQL"
|
||||||
homepage "https://github.com/AdrianBonpin/gridline"
|
homepage "https://git.ranio.xyz/adrianbonpin/gridline"
|
||||||
|
|
||||||
depends_on macos: ">= :monterey"
|
depends_on macos: :monterey
|
||||||
|
|
||||||
app "Gridline.app"
|
app "Gridline.app"
|
||||||
|
|
||||||
# The "installation step" — strip the quarantine flag so the unsigned
|
|
||||||
# (ad-hoc signed) app launches without the Gatekeeper prompt.
|
|
||||||
postflight do
|
|
||||||
if OS.mac?
|
|
||||||
system_command "/usr/bin/xattr",
|
|
||||||
args: ["-dr", "com.apple.quarantine", "#{appdir}/Gridline.app"]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
zap trash: [
|
zap trash: [
|
||||||
"~/Library/Application Support/com.adrianbonpin.gridline",
|
"~/Library/Application Support/com.adrianbonpin.gridline",
|
||||||
"~/Library/Caches/com.adrianbonpin.gridline",
|
"~/Library/Caches/com.adrianbonpin.gridline",
|
||||||
|
|||||||
+77
-1
@@ -51,6 +51,58 @@ get_token() {
|
|||||||
|
|
||||||
GITEA_TOKEN="$(get_token)"
|
GITEA_TOKEN="$(get_token)"
|
||||||
|
|
||||||
|
# ---- macOS signing + notarization -----------------------------------------
|
||||||
|
# Requires a "Developer ID Application" certificate in the login keychain and
|
||||||
|
# an App Store Connect API key for notarization. See the README's "macOS
|
||||||
|
# signing" section for how to create both.
|
||||||
|
#
|
||||||
|
# Notarization credentials are read from ~/.config/gridline/notarize.env
|
||||||
|
# (never committed). Create it with:
|
||||||
|
# APPLE_API_KEY=<Key ID>
|
||||||
|
# APPLE_API_ISSUER=<Issuer ID>
|
||||||
|
# APPLE_API_KEY_PATH=/absolute/path/to/AuthKey_<KeyID>.p8
|
||||||
|
# (or drop the .p8 at ~/.appstoreconnect/private_keys/AuthKey_<KeyID>.p8 and
|
||||||
|
# omit APPLE_API_KEY_PATH — tauri auto-searches that location.)
|
||||||
|
|
||||||
|
# 1. Resolve the Developer ID Application signing identity from the keychain.
|
||||||
|
SIGNING_IDENTITY="$(security find-identity -v -p codesigning 2>/dev/null \
|
||||||
|
| grep -i 'Developer ID Application' \
|
||||||
|
| sed -E 's/.*"([^"]+)".*/\1/' \
|
||||||
|
| head -1 || true)"
|
||||||
|
if [ -z "$SIGNING_IDENTITY" ]; then
|
||||||
|
echo "ERROR: No 'Developer ID Application' certificate found in the login keychain." >&2
|
||||||
|
echo " Create one at https://developer.apple.com/account/resources/certificates/list" >&2
|
||||||
|
echo " (Certificates, IDs & Profiles -> + -> Developer ID Application), download the" >&2
|
||||||
|
echo " .cer, and double-click it to install into your login keychain." >&2
|
||||||
|
echo " Verify with: security find-identity -v -p codesigning" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export APPLE_SIGNING_IDENTITY="$SIGNING_IDENTITY"
|
||||||
|
echo ">> Signing identity: $SIGNING_IDENTITY"
|
||||||
|
|
||||||
|
# 2. Load notarization credentials.
|
||||||
|
NOTARIZE_ENV="${HOME}/.config/gridline/notarize.env"
|
||||||
|
if [ -f "$NOTARIZE_ENV" ]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$NOTARIZE_ENV"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then
|
||||||
|
if [ "${GRIDLINE_SKIP_NOTARIZE:-0}" = "1" ]; then
|
||||||
|
echo ">> WARNING: notarization credentials missing; GRIDLINE_SKIP_NOTARIZE=1 set." >&2
|
||||||
|
echo " Building signed but NOT notarized (test-only — users will still see a prompt)." >&2
|
||||||
|
else
|
||||||
|
echo "ERROR: Notarization credentials not configured." >&2
|
||||||
|
echo " Create ${NOTARIZE_ENV} with APPLE_API_KEY and APPLE_API_ISSUER" >&2
|
||||||
|
echo " (and APPLE_API_KEY_PATH, or place the .p8 at ~/.appstoreconnect/private_keys/)." >&2
|
||||||
|
echo " See the README's 'macOS signing' section for the App Store Connect API key setup." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ">> Notarization: API key ${APPLE_API_KEY} (issuer ${APPLE_API_ISSUER})"
|
||||||
|
fi
|
||||||
|
|
||||||
# ---- build bundled DB tools (macOS) ----------------------------------------
|
# ---- build bundled DB tools (macOS) ----------------------------------------
|
||||||
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
|
WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
OUT_PG="${WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
OUT_PG="${WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
||||||
@@ -75,6 +127,12 @@ for b in pg_dump pg_restore psql; do
|
|||||||
libpq=$(otool -L "$OUT_PG/$b" | awk '/libpq/ {print $1; exit}')
|
libpq=$(otool -L "$OUT_PG/$b" | awk '/libpq/ {print $1; exit}')
|
||||||
install_name_tool -change "$libpq" "@loader_path/libpq.5.dylib" "$OUT_PG/$b"
|
install_name_tool -change "$libpq" "@loader_path/libpq.5.dylib" "$OUT_PG/$b"
|
||||||
done
|
done
|
||||||
|
# install_name_tool invalidates the linker's signature; re-sign with the
|
||||||
|
# Developer ID identity + hardened runtime + secure timestamp so the bundle
|
||||||
|
# passes notarization (Apple rejects ad-hoc-signed nested executables).
|
||||||
|
for f in "$OUT_PG"/pg_dump "$OUT_PG"/pg_restore "$OUT_PG"/psql "$OUT_PG"/libpq.5.dylib; do
|
||||||
|
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
echo ">> Building MariaDB client tools..."
|
echo ">> Building MariaDB client tools..."
|
||||||
mkdir -p "$OUT_MY"
|
mkdir -p "$OUT_MY"
|
||||||
@@ -112,7 +170,7 @@ for pass in 1 2 3 4 5 6; do
|
|||||||
if [ ! -f "$OUT_MY/$base" ]; then cp "$lib" "$OUT_MY/$base" 2>/dev/null || true; fi
|
if [ ! -f "$OUT_MY/$base" ]; then cp "$lib" "$OUT_MY/$base" 2>/dev/null || true; fi
|
||||||
install_name_tool -change "$lib" "@loader_path/$base" "$dylib" 2>/dev/null || true
|
install_name_tool -change "$lib" "@loader_path/$base" "$dylib" 2>/dev/null || true
|
||||||
done || true
|
done || true
|
||||||
codesign --force --sign - "$dylib" 2>/dev/null || true
|
codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$dylib"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -130,6 +188,24 @@ VERSION="${TAG#v}"
|
|||||||
BUNDLE_ARM="${WORKSPACE}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle"
|
BUNDLE_ARM="${WORKSPACE}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle"
|
||||||
BUNDLE_INTEL="${WORKSPACE}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle"
|
BUNDLE_INTEL="${WORKSPACE}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle"
|
||||||
|
|
||||||
|
# ---- verify signing + notarization -----------------------------------------
|
||||||
|
# Confirm both .app bundles are properly signed and carry a stapled
|
||||||
|
# notarization ticket before we upload them. spctl can lag right after
|
||||||
|
# notarization (assessment cache), so stapler validate is the authoritative
|
||||||
|
# check; a pending spctl is only a warning.
|
||||||
|
for APP in \
|
||||||
|
"${BUNDLE_ARM}/macos/Gridline.app" \
|
||||||
|
"${BUNDLE_INTEL}/macos/Gridline.app"; do
|
||||||
|
[ -d "$APP" ] || { echo "missing $APP" >&2; exit 1; }
|
||||||
|
echo ">> Verifying $APP"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP" \
|
||||||
|
|| { echo "codesign verify FAILED" >&2; exit 1; }
|
||||||
|
xcrun stapler validate "$APP" \
|
||||||
|
|| { echo "notarization ticket not stapled" >&2; exit 1; }
|
||||||
|
spctl --assess --type execute --verbose=4 "$APP" \
|
||||||
|
|| echo " (spctl assessment pending — normal right after notarization; stapler validate passed)"
|
||||||
|
done
|
||||||
|
|
||||||
# ---- create/ensure release + upload DMGs -----------------------------------
|
# ---- create/ensure release + upload DMGs -----------------------------------
|
||||||
API="${GITEA_SERVER}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}"
|
API="${GITEA_SERVER}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}"
|
||||||
AUTH="Authorization: token ${GITEA_TOKEN}"
|
AUTH="Authorization: token ${GITEA_TOKEN}"
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ export default defineConfig({
|
|||||||
compressHTML: true,
|
compressHTML: true,
|
||||||
trailingSlash: 'ignore',
|
trailingSlash: 'ignore',
|
||||||
integrations: [
|
integrations: [
|
||||||
sitemap({
|
sitemap(),
|
||||||
lastmod: new Date(),
|
|
||||||
}),
|
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
vite: {
|
vite: {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* /404.html 404
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 1.5 KiB |
+31
-8
@@ -1,9 +1,32 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
<style>
|
<svg width="100%" height="100%" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
|
||||||
path { fill: #000; }
|
<g transform="matrix(1.464129,0,0,1.464129,-67.349927,-273.792094)">
|
||||||
@media (prefers-color-scheme: dark) {
|
<path d="M729,357.75L729,699.25C729,793.489 652.489,870 558.25,870L216.75,870C122.511,870 46,793.489 46,699.25L46,357.75C46,263.511 122.511,187 216.75,187L558.25,187C652.489,187 729,263.511 729,357.75Z" style="fill:url(#_Linear1);stroke:black;stroke-width:0.68px;"/>
|
||||||
path { fill: #FFF; }
|
</g>
|
||||||
}
|
<g id="Icon">
|
||||||
</style>
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M325,351C325,305.743 403.415,269 500,269C596.585,269 675,305.743 675,351C675,365.931 666.465,379.936 651.557,392C640.124,401.252 624.943,409.364 606.99,415.876C577.4,426.609 540.28,433 500,433C459.711,433 422.583,426.607 392.99,415.869C375.046,409.358 359.872,401.249 348.443,392C333.535,379.936 325,365.931 325,351Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M325,433L325,351C325,365.931 333.535,379.936 348.443,392C359.872,401.249 375.046,409.358 392.99,415.869L392.99,497.869C375.046,491.358 359.872,483.249 348.443,474C333.535,461.936 325,447.931 325,433Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<path d="M626.996,592.007C591.874,604.748 547.812,612.333 500,612.333L500,515C547.812,515 591.874,507.414 626.996,494.674C648.307,486.944 666.326,477.316 679.897,466.333C697.592,452.013 707.724,435.39 707.724,417.667L707.724,515C707.724,532.723 697.592,549.347 679.897,563.667C666.326,574.649 648.307,584.277 626.996,592.007Z" style="fill:white;stroke:black;stroke-width:16.67px;"/>
|
||||||
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M325,515L325,433C325,447.931 333.535,461.936 348.443,474C359.872,483.249 375.046,491.358 392.99,497.869L392.99,579.869C375.046,573.358 359.872,565.249 348.443,556C333.535,543.936 325,529.931 325,515Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M325,597L325,515C325,529.931 333.535,543.936 348.443,556C359.872,565.249 375.046,573.358 392.99,579.869L392.99,661.869C375.046,655.358 359.872,647.249 348.443,638C333.535,625.936 325,611.931 325,597Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M675,515L675,597C675,611.931 666.465,625.936 651.557,638C640.124,647.252 624.943,655.364 606.99,661.876L606.99,579.876C624.943,573.364 640.124,565.252 651.557,556C666.465,543.936 675,529.931 675,515Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1.186992,0,0,1.186992,-93.495935,-96.300813)">
|
||||||
|
<path d="M675,597L675,679C675,705.383 648.352,728.872 606.99,743.876L606.99,661.876C624.943,655.364 640.124,647.252 651.557,638C666.465,625.936 675,611.931 675,597Z" style="fill:white;stroke:black;stroke-width:14.04px;"/>
|
||||||
|
</g>
|
||||||
|
<path d="M372.98,786.665C323.898,768.856 292.276,740.978 292.276,709.667L292.276,612.333C292.276,630.057 302.408,646.68 320.103,661C333.669,671.979 351.68,681.603 372.98,689.332C408.107,702.078 452.177,709.667 500,709.667C547.812,709.667 591.874,702.081 626.996,689.341L626.996,786.674C591.874,799.414 547.812,807 500,807C452.177,807 408.107,799.411 372.98,786.665Z" style="fill:white;stroke:black;stroke-width:16.67px;"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(683,683,-683,683,46,187)"><stop offset="0" style="stop-color:rgb(108,108,108);stop-opacity:1"/><stop offset="0.17" style="stop-color:rgb(15,15,15);stop-opacity:1"/><stop offset="0.5" style="stop-color:black;stop-opacity:1"/><stop offset="0.83" style="stop-color:rgb(15,15,15);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(108,108,108);stop-opacity:1"/></linearGradient>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 4.4 KiB |
+5
-22
@@ -59,18 +59,15 @@
|
|||||||
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(userAgent);
|
const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(userAgent);
|
||||||
const keepLabel = downloadBtn.hasAttribute("data-download-label");
|
const keepLabel = downloadBtn.hasAttribute("data-download-label");
|
||||||
|
|
||||||
const releasesUrl = "https://github.com/AdrianBonpin/gridline/releases";
|
const releasesUrl = "https://git.ranio.xyz/adrianbonpin/gridline/releases";
|
||||||
const version = "0.7.10";
|
const version = "0.7.14";
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
downloadBtn.href = releasesUrl;
|
downloadBtn.href = releasesUrl;
|
||||||
if (!keepLabel) downloadBtn.textContent = "Get it on GitHub";
|
if (!keepLabel) downloadBtn.textContent = "Get it on Gitea";
|
||||||
} else if (isMac) {
|
} else if (isMac) {
|
||||||
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_aarch64.dmg`;
|
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_aarch64.dmg`;
|
||||||
if (!keepLabel) downloadBtn.textContent = "Download for macOS";
|
if (!keepLabel) downloadBtn.textContent = "Download for macOS";
|
||||||
if (downloadBtn.id === "download") {
|
|
||||||
document.getElementById("macos-hint")?.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
} else if (isWin) {
|
} else if (isWin) {
|
||||||
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_x64-setup.exe`;
|
downloadBtn.href = `${releasesUrl}/download/v${version}/Gridline_${version}_x64-setup.exe`;
|
||||||
if (!keepLabel) downloadBtn.textContent = "Download for Windows";
|
if (!keepLabel) downloadBtn.textContent = "Download for Windows";
|
||||||
@@ -83,18 +80,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("copy-quarantine")?.addEventListener("click", async () => {
|
|
||||||
const command = "sudo xattr -dr com.apple.quarantine /Applications/Gridline.app";
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(command);
|
|
||||||
const btn = document.getElementById("copy-quarantine");
|
|
||||||
if (!btn) return;
|
|
||||||
const original = btn.textContent;
|
|
||||||
btn.textContent = "Copied";
|
|
||||||
setTimeout(() => (btn.textContent = original), 1500);
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Nav scroll background
|
// Nav scroll background
|
||||||
const nav = document.getElementById("nav");
|
const nav = document.getElementById("nav");
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
@@ -141,24 +126,22 @@
|
|||||||
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
|
const subtitle = hero.querySelector(".hero-animate > p:nth-of-type(2)");
|
||||||
const buttons = hero.querySelector(".hero-animate > div");
|
const buttons = hero.querySelector(".hero-animate > div");
|
||||||
const screenshot = hero.querySelector(".hero-screenshot");
|
const screenshot = hero.querySelector(".hero-screenshot");
|
||||||
const macosHint = hero.querySelector("#macos-hint");
|
|
||||||
|
|
||||||
if (reducedMotion) {
|
if (reducedMotion) {
|
||||||
// Reduced motion: reveal the hero immediately (CSS hides it at first
|
// Reduced motion: reveal the hero immediately (CSS hides it at first
|
||||||
// paint to avoid flicker, but the timeline below is skipped). Section
|
// paint to avoid flicker, but the timeline below is skipped). Section
|
||||||
// reveals below still run through the IntersectionObserver.
|
// reveals below still run through the IntersectionObserver.
|
||||||
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 1, translateY: 0 });
|
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 1, translateY: 0 });
|
||||||
} else {
|
} else {
|
||||||
const heroTl = anime.timeline({ easing: "easeOutExpo" });
|
const heroTl = anime.timeline({ easing: "easeOutExpo" });
|
||||||
|
|
||||||
anime.set([eyebrow, h1, subtitle, buttons, screenshot, macosHint], { opacity: 0, translateY: 20 });
|
anime.set([eyebrow, h1, subtitle, buttons, screenshot], { opacity: 0, translateY: 20 });
|
||||||
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
|
anime.set(screenshot, { opacity: 0, scale: 0.96, translateY: 0 });
|
||||||
|
|
||||||
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
|
if (eyebrow) heroTl.add({ targets: eyebrow, opacity: [0, 1], translateY: [15, 0] }, 0);
|
||||||
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
|
if (h1) heroTl.add({ targets: h1, opacity: [0, 1], translateY: [30, 0], duration: 800 }, 100);
|
||||||
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
|
if (subtitle) heroTl.add({ targets: subtitle, opacity: [0, 1], translateY: [20, 0] }, 250);
|
||||||
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
|
if (buttons) heroTl.add({ targets: buttons, opacity: [0, 1], translateY: [20, 0] }, 350);
|
||||||
if (macosHint) heroTl.add({ targets: macosHint, opacity: [0, 1], translateY: [20, 0] }, 400);
|
|
||||||
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
|
if (screenshot) heroTl.add({ targets: screenshot, opacity: [0, 1], scale: [0.96, 1], duration: 900 }, 450);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
"sizes": "any",
|
"sizes": "any",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/apple-touch-icon.png",
|
||||||
|
"sizes": "180x180",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,30 @@ export interface ChangelogEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const changelogEntries: ChangelogEntry[] = [
|
export const changelogEntries: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.7.14",
|
||||||
|
date: "2026-09-04",
|
||||||
|
highlights: [
|
||||||
|
"No more repeated macOS keychain prompts — saved connection passwords and SSH secrets are now ACL-pinned to the app's code signature when stored, so app upgrades never re-trigger the macOS keychain access dialog (users upgrading from older builds see one final prompt — choose Always Allow).",
|
||||||
|
"Install via Homebrew — add the Gitea tap (brew tap AdrianBonpin/gridline https://git.ranio.xyz/adrianbonpin/homebrew-gridline.git) and run brew install --cask gridline.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.7.13",
|
||||||
|
date: "2026-09-04",
|
||||||
|
highlights: [
|
||||||
|
"macOS is now Developer-ID signed and notarized — the app is properly code-signed with a Developer ID Application certificate and notarized by Apple, so macOS opens it without the Gatekeeper 'Open Anyway' prompt or the xattr quarantine workaround.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: "0.7.12",
|
||||||
|
date: "2026-09-01",
|
||||||
|
highlights: [
|
||||||
|
"Reliable cell editing for non-text columns — editing a PostgreSQL cell whose type isn't text (integers, booleans, UUIDs, json/jsonb, timestamps, enums, numerics) now works; edited values are sent to PostgreSQL in text wire format so the server parses them into the column's own type.",
|
||||||
|
"Recent connections fix — the Recent strip no longer renders empty on first launch when the connections list hadn't loaded yet.",
|
||||||
|
"Full CI release pipeline for Windows — the self-hosted Windows runner now builds the .exe/.msi installers (bundled pg_dump/pg_restore/psql + MariaDB clients) and uploads them to the release alongside Linux.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.7.10",
|
version: "0.7.10",
|
||||||
date: "2026-08-13",
|
date: "2026-08-13",
|
||||||
|
|||||||
@@ -36,8 +36,13 @@ const isProd = import.meta.env.PROD;
|
|||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/gridline-icon.svg" />
|
||||||
<link rel="apple-touch-icon" href="/gridline-icon.svg" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
<link rel="preload" as="image" href="/og.png" />
|
||||||
|
<link rel="preconnect" href="https://stat.ranio.xyz" crossorigin />
|
||||||
|
<link rel="preconnect" href="https://git.ranio.xyz" />
|
||||||
|
<link rel="dns-prefetch" href="https://stat.ranio.xyz" />
|
||||||
|
<link rel="dns-prefetch" href="https://git.ranio.xyz" />
|
||||||
|
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
@@ -54,6 +59,7 @@ const isProd = import.meta.env.PROD;
|
|||||||
<meta name="twitter:title" content={title} />
|
<meta name="twitter:title" content={title} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
<meta name="twitter:image" content={ogImageURL} />
|
<meta name="twitter:image" content={ogImageURL} />
|
||||||
|
<meta name="twitter:image:alt" content="Gridline — the open-source database GUI" />
|
||||||
<meta name="twitter:site" content="@AdrianBonpin" />
|
<meta name="twitter:site" content="@AdrianBonpin" />
|
||||||
<meta name="twitter:creator" content="@AdrianBonpin" />
|
<meta name="twitter:creator" content="@AdrianBonpin" />
|
||||||
|
|
||||||
@@ -98,7 +104,7 @@ const isProd = import.meta.env.PROD;
|
|||||||
</a>
|
</a>
|
||||||
<div class="hidden md:flex items-center gap-6 text-sm">
|
<div class="hidden md:flex items-center gap-6 text-sm">
|
||||||
<a href="/#features" class="nav-link">Features</a>
|
<a href="/#features" class="nav-link">Features</a>
|
||||||
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="nav-link">GitHub</a>
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline" target="_blank" rel="noopener" class="nav-link">Gitea</a>
|
||||||
<a href="/#download" data-download data-download-label class="nav-link">Download</a>
|
<a href="/#download" data-download data-download-label class="nav-link">Download</a>
|
||||||
<button id="theme-toggle" class="p-2 rounded-full hover:bg-[var(--surface-elevated)] transition-colors" aria-label="Toggle theme">
|
<button id="theme-toggle" class="p-2 rounded-full hover:bg-[var(--surface-elevated)] transition-colors" aria-label="Toggle theme">
|
||||||
<span class="theme-icon-sun hidden">☀</span>
|
<span class="theme-icon-sun hidden">☀</span>
|
||||||
@@ -133,9 +139,9 @@ const isProd = import.meta.env.PROD;
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-sm mb-4">Project</h4>
|
<h4 class="font-semibold text-sm mb-4">Project</h4>
|
||||||
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
|
<ul class="space-y-2 text-sm text-[var(--text-muted)]">
|
||||||
<li><a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="footer-link">GitHub</a></li>
|
<li><a href="https://git.ranio.xyz/adrianbonpin/gridline" target="_blank" rel="noopener" class="footer-link">Gitea</a></li>
|
||||||
<li><a href="https://github.com/AdrianBonpin/gridline/issues" target="_blank" rel="noopener" class="footer-link">Issues</a></li>
|
<li><a href="https://git.ranio.xyz/adrianbonpin/gridline/issues" target="_blank" rel="noopener" class="footer-link">Issues</a></li>
|
||||||
<li><a href="https://github.com/AdrianBonpin/gridline/blob/prod/ROADMAP.md" target="_blank" rel="noopener" class="footer-link">Roadmap</a></li>
|
<li><a href="https://git.ranio.xyz/adrianbonpin/gridline/src/branch/prod/ROADMAP.md" target="_blank" rel="noopener" class="footer-link">Roadmap</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+18
-3
@@ -6,6 +6,7 @@ import {
|
|||||||
buildWebSiteJsonLd,
|
buildWebSiteJsonLd,
|
||||||
buildBreadcrumbJsonLd,
|
buildBreadcrumbJsonLd,
|
||||||
buildTitle,
|
buildTitle,
|
||||||
|
buildProductJsonLd,
|
||||||
} from "./seo";
|
} from "./seo";
|
||||||
|
|
||||||
describe("seo builders", () => {
|
describe("seo builders", () => {
|
||||||
@@ -19,7 +20,7 @@ describe("seo builders", () => {
|
|||||||
applicationCategory: "DeveloperApplication",
|
applicationCategory: "DeveloperApplication",
|
||||||
offersPrice: 0,
|
offersPrice: 0,
|
||||||
offersCurrency: "USD",
|
offersCurrency: "USD",
|
||||||
downloadUrl: "https://github.com/AdrianBonpin/gridline/releases",
|
downloadUrl: "https://git.ranio.xyz/adrianbonpin/gridline/releases",
|
||||||
});
|
});
|
||||||
expect(ld["@type"]).toBe("SoftwareApplication");
|
expect(ld["@type"]).toBe("SoftwareApplication");
|
||||||
expect(ld.name).toBe("Gridline");
|
expect(ld.name).toBe("Gridline");
|
||||||
@@ -42,10 +43,10 @@ describe("seo builders", () => {
|
|||||||
name: "Gridline",
|
name: "Gridline",
|
||||||
url: "https://getgridline.app/",
|
url: "https://getgridline.app/",
|
||||||
logo: "https://getgridline.app/gridline-icon.svg",
|
logo: "https://getgridline.app/gridline-icon.svg",
|
||||||
sameAs: ["https://github.com/AdrianBonpin/gridline"],
|
sameAs: ["https://git.ranio.xyz/adrianbonpin/gridline"],
|
||||||
});
|
});
|
||||||
expect(ld["@type"]).toBe("Organization");
|
expect(ld["@type"]).toBe("Organization");
|
||||||
expect(ld.sameAs).toContain("https://github.com/AdrianBonpin/gridline");
|
expect(ld.sameAs).toContain("https://git.ranio.xyz/adrianbonpin/gridline");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("buildWebSiteJsonLd includes SearchAction", () => {
|
it("buildWebSiteJsonLd includes SearchAction", () => {
|
||||||
@@ -70,4 +71,18 @@ describe("seo builders", () => {
|
|||||||
it("buildTitle appends the brand", () => {
|
it("buildTitle appends the brand", () => {
|
||||||
expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline");
|
expect(buildTitle("PostgreSQL GUI")).toBe("PostgreSQL GUI | Gridline");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("buildProductJsonLd returns valid Product schema", () => {
|
||||||
|
const ld = buildProductJsonLd({
|
||||||
|
name: "Gridline",
|
||||||
|
url: "https://getgridline.app/",
|
||||||
|
description: "Open-source database GUI",
|
||||||
|
image: "https://getgridline.app/og.png",
|
||||||
|
offersPrice: 0,
|
||||||
|
offersCurrency: "USD",
|
||||||
|
});
|
||||||
|
expect(ld["@type"]).toBe("Product");
|
||||||
|
expect(ld.name).toBe("Gridline");
|
||||||
|
expect((ld.offers as { price: number }).price).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,3 +108,26 @@ export function buildBreadcrumbJsonLd(
|
|||||||
export function buildTitle(primary: string, brand = "Gridline"): string {
|
export function buildTitle(primary: string, brand = "Gridline"): string {
|
||||||
return `${primary} | ${brand}`;
|
return `${primary} | ${brand}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildProductJsonLd(opts: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
offersPrice: number;
|
||||||
|
offersCurrency: string;
|
||||||
|
}): JsonLd {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Product",
|
||||||
|
name: opts.name,
|
||||||
|
url: opts.url,
|
||||||
|
description: opts.description,
|
||||||
|
image: opts.image,
|
||||||
|
offers: {
|
||||||
|
"@type": "Offer",
|
||||||
|
price: opts.offersPrice,
|
||||||
|
priceCurrency: opts.offersCurrency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ const jsonLd = [
|
|||||||
|
|
||||||
<p class="mt-8 text-sm text-[var(--text-muted)]">
|
<p class="mt-8 text-sm text-[var(--text-muted)]">
|
||||||
Full release history on{" "}
|
Full release history on{" "}
|
||||||
<a href="https://github.com/AdrianBonpin/gridline/releases" target="_blank" rel="noopener" class="text-[var(--primary)] hover:text-[var(--primary-hover)]">
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline/releases" target="_blank" rel="noopener" class="text-[var(--primary)] hover:text-[var(--primary-hover)]">
|
||||||
GitHub Releases
|
Gitea Releases
|
||||||
</a>.
|
</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+23
-17
@@ -6,6 +6,8 @@ import {
|
|||||||
buildFaqJsonLd,
|
buildFaqJsonLd,
|
||||||
buildOrganizationJsonLd,
|
buildOrganizationJsonLd,
|
||||||
buildWebSiteJsonLd,
|
buildWebSiteJsonLd,
|
||||||
|
buildProductJsonLd,
|
||||||
|
buildBreadcrumbJsonLd,
|
||||||
} from "../lib/seo";
|
} from "../lib/seo";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import HeroDbViewer from "../components/hero/HeroDbViewer";
|
import HeroDbViewer from "../components/hero/HeroDbViewer";
|
||||||
@@ -13,7 +15,7 @@ import queryEditor from "../assets/screenshots/query-editor.png";
|
|||||||
import erDiagram from "../assets/screenshots/er-diagram.png";
|
import erDiagram from "../assets/screenshots/er-diagram.png";
|
||||||
import home from "../assets/screenshots/home.png";
|
import home from "../assets/screenshots/home.png";
|
||||||
import dataGrid from "../assets/screenshots/data-grid.png";
|
import dataGrid from "../assets/screenshots/data-grid.png";
|
||||||
const version = "0.7.11";
|
const version = "0.7.14";
|
||||||
const site = "https://getgridline.app";
|
const site = "https://getgridline.app";
|
||||||
const jsonLd = [
|
const jsonLd = [
|
||||||
buildSoftwareApplicationJsonLd({
|
buildSoftwareApplicationJsonLd({
|
||||||
@@ -26,7 +28,7 @@ const jsonLd = [
|
|||||||
applicationCategory: "DeveloperApplication",
|
applicationCategory: "DeveloperApplication",
|
||||||
offersPrice: 0,
|
offersPrice: 0,
|
||||||
offersCurrency: "USD",
|
offersCurrency: "USD",
|
||||||
downloadUrl: "https://github.com/AdrianBonpin/gridline/releases",
|
downloadUrl: "https://git.ranio.xyz/adrianbonpin/gridline/releases",
|
||||||
}),
|
}),
|
||||||
buildFaqJsonLd(faqItems),
|
buildFaqJsonLd(faqItems),
|
||||||
buildOrganizationJsonLd({
|
buildOrganizationJsonLd({
|
||||||
@@ -34,7 +36,7 @@ const jsonLd = [
|
|||||||
url: `${site}/`,
|
url: `${site}/`,
|
||||||
logo: `${site}/gridline-icon.svg`,
|
logo: `${site}/gridline-icon.svg`,
|
||||||
sameAs: [
|
sameAs: [
|
||||||
"https://github.com/AdrianBonpin/gridline",
|
"https://git.ranio.xyz/adrianbonpin/gridline",
|
||||||
"https://x.com/AdrianBonpin",
|
"https://x.com/AdrianBonpin",
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -42,9 +44,21 @@ const jsonLd = [
|
|||||||
name: "Gridline",
|
name: "Gridline",
|
||||||
url: `${site}/`,
|
url: `${site}/`,
|
||||||
}),
|
}),
|
||||||
|
buildProductJsonLd({
|
||||||
|
name: "Gridline",
|
||||||
|
url: `${site}/`,
|
||||||
|
description:
|
||||||
|
"The open-source database GUI for PostgreSQL, MySQL, SQLite, and Redis. No paywalls, no tab limits, fully open source.",
|
||||||
|
image: `${site}/og.png`,
|
||||||
|
offersPrice: 0,
|
||||||
|
offersCurrency: "USD",
|
||||||
|
}),
|
||||||
|
buildBreadcrumbJsonLd([
|
||||||
|
{ name: "Home", url: `${site}/` },
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
<Layout title="Gridline — Open-Source Database GUI" jsonLd={jsonLd}>
|
<Layout title="Gridline — Free Open-Source Database GUI" jsonLd={jsonLd}>
|
||||||
<script src="/landing.js" defer></script>
|
<script src="/landing.js" defer></script>
|
||||||
|
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
@@ -59,21 +73,13 @@ const jsonLd = [
|
|||||||
Query, explore, sync, and edit PostgreSQL, MySQL, SQLite, and Redis — with no paywalls, no tab limits, no account required.
|
Query, explore, sync, and edit PostgreSQL, MySQL, SQLite, and Redis — with no paywalls, no tab limits, no account required.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||||
<a id="download" href="https://github.com/AdrianBonpin/gridline/releases" class="primary-btn" data-download>
|
<a id="download" href="https://git.ranio.xyz/adrianbonpin/gridline/releases" class="primary-btn" data-download>
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
|
||||||
View on GitHub
|
View on Gitea
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="macos-hint" class="hidden max-w-lg mx-auto w-full rounded-xl border border-[var(--border)] bg-[var(--surface)] p-3 text-left mt-4">
|
|
||||||
<p class="text-xs text-[var(--text-muted)] mb-2">First launch on macOS? Run this in Terminal:</p>
|
|
||||||
<div class="flex items-center justify-between gap-3 rounded-lg bg-[var(--bg)] border border-[var(--border)] px-3 py-2">
|
|
||||||
<code class="font-mono text-xs truncate">sudo xattr -dr com.apple.quarantine /Applications/Gridline.app</code>
|
|
||||||
<button id="copy-quarantine" class="text-xs font-medium text-[var(--primary)] hover:text-[var(--primary-hover)] transition-colors shrink-0" aria-label="Copy command">Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-6xl">
|
<div class="hero-screenshot mt-14 md:mt-20 mx-auto max-w-6xl">
|
||||||
@@ -174,8 +180,8 @@ const jsonLd = [
|
|||||||
<p class="text-[var(--text-muted)] mb-8 max-w-md mx-auto">
|
<p class="text-[var(--text-muted)] mb-8 max-w-md mx-auto">
|
||||||
Gridline is Apache 2.0 and developed openly. Contributions, issues, and feature requests are welcome.
|
Gridline is Apache 2.0 and developed openly. Contributions, issues, and feature requests are welcome.
|
||||||
</p>
|
</p>
|
||||||
<a href="https://github.com/AdrianBonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
|
<a href="https://git.ranio.xyz/adrianbonpin/gridline" target="_blank" rel="noopener" class="ghost-btn">
|
||||||
Star on GitHub
|
Star on Gitea
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user