mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
Merge branch 'main' of github.com:dograh-hq/dograh
This commit is contained in:
commit
9298116887
11 changed files with 314 additions and 17 deletions
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
".": "1.25.0"
|
||||
".": "1.26.0"
|
||||
}
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -1,5 +1,18 @@
|
|||
# Changelog
|
||||
|
||||
## [1.26.0](https://github.com/dograh-hq/dograh/compare/dograh-v1.25.0...dograh-v1.26.0) (2026-04-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* refactor node spec and add mcp tools ([#244](https://github.com/dograh-hq/dograh/issues/244)) ([00a1a22](https://github.com/dograh-hq/dograh/commit/00a1a22b749dab5b828ee529d7272cbdaaeb9aca))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* compare dirty against correct baseline ([6606a7f](https://github.com/dograh-hq/dograh/commit/6606a7f901f351d5832ebc27a0900c1195a4090c))
|
||||
* fix slack community URL ([86026f5](https://github.com/dograh-hq/dograh/commit/86026f5c6ffdbfbef96cec8aff6c4265f061b77e))
|
||||
|
||||
## [1.25.0](https://github.com/dograh-hq/dograh/compare/dograh-v1.24.0...dograh-v1.25.0) (2026-04-17)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[project]
|
||||
name = "dograh-api"
|
||||
version = "1.25.0"
|
||||
version = "1.26.0"
|
||||
description = "Backend API for Dograh voice AI platform"
|
||||
requires-python = ">=3.12"
|
||||
|
|
|
|||
113
docs/deployment/update.mdx
Normal file
113
docs/deployment/update.mdx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
title: "Update"
|
||||
description: "Update your self-hosted Dograh stack to a newer image version"
|
||||
---
|
||||
|
||||
This guide covers updating a Dograh stack you've already deployed with [Docker](/deployment/docker) or a [custom domain](/deployment/custom-domain). You run commands from the same directory that contains your `docker-compose.yaml` (this is the `dograh/` directory if you used `setup_remote.sh`).
|
||||
|
||||
## Find an image version
|
||||
|
||||
Dograh publishes two images — `dograh-api` and `dograh-ui` — to both container registries:
|
||||
|
||||
- **GitHub Container Registry** — [github.com/orgs/dograh-hq/packages](https://github.com/orgs/dograh-hq/packages)
|
||||
- **Docker Hub** — [hub.docker.com/u/dograhai](https://hub.docker.com/u/dograhai)
|
||||
|
||||
Each release is published under two kinds of tags:
|
||||
|
||||
| Tag style | Example | When to use |
|
||||
|-----------|---------|-------------|
|
||||
| **Release tag** | `v0.8.2` | Stable, recommended for production |
|
||||
| **Git commit SHA** | `a1b2c3d` | Bleeding edge — any commit merged to `main` |
|
||||
| `latest` | `latest` | Tracks the most recent release tag |
|
||||
|
||||
<Warning>
|
||||
Always update **`dograh-api`** and **`dograh-ui`** to the **same tag**. The two images are built from the same commit and the UI expects API responses in a matching shape — mixing versions will break the app.
|
||||
</Warning>
|
||||
|
||||
## Option A: Update to the latest release
|
||||
|
||||
If your `docker-compose.yaml` uses `:latest` (the default), just pull and restart:
|
||||
|
||||
<CodeGroup>
|
||||
```bash Local deployment
|
||||
docker compose down
|
||||
docker compose up --pull always
|
||||
```
|
||||
```bash Remote deployment
|
||||
cd dograh
|
||||
sudo docker compose --profile remote down
|
||||
sudo docker compose --profile remote up --pull always
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
`--pull always` forces Docker to fetch the latest `:latest` from the registry instead of reusing your cached image.
|
||||
|
||||
## Option B: Pin a specific tag
|
||||
|
||||
To update (or roll back) to a specific release or commit, edit `docker-compose.yaml` and change the `image:` lines for both `api` and `ui` services to the same tag.
|
||||
|
||||
Open the file:
|
||||
|
||||
```bash
|
||||
nano docker-compose.yaml
|
||||
```
|
||||
|
||||
Find these two lines:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: ${REGISTRY:-dograhai}/dograh-api:latest
|
||||
ui:
|
||||
image: ${REGISTRY:-dograhai}/dograh-ui:latest
|
||||
```
|
||||
|
||||
Replace `:latest` with your chosen tag on **both** services — for example:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
image: ${REGISTRY:-dograhai}/dograh-api:v0.8.2
|
||||
ui:
|
||||
image: ${REGISTRY:-dograhai}/dograh-ui:v0.8.2
|
||||
```
|
||||
|
||||
<Note>
|
||||
You can use either registry. Leave `REGISTRY` unset for Docker Hub (`dograhai`), or export `REGISTRY=ghcr.io/dograh-hq` to pull from GitHub Container Registry.
|
||||
</Note>
|
||||
|
||||
Then bring the stack down and back up:
|
||||
|
||||
<CodeGroup>
|
||||
```bash Local deployment
|
||||
docker compose down
|
||||
docker compose up --pull always
|
||||
```
|
||||
```bash Remote deployment
|
||||
cd dograh
|
||||
sudo docker compose --profile remote down
|
||||
sudo docker compose --profile remote up --pull always
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Verify the update
|
||||
|
||||
Check the running image tags:
|
||||
|
||||
```bash
|
||||
docker compose ps --format "table {{.Service}}\t{{.Image}}"
|
||||
```
|
||||
|
||||
You should see the API and UI both running the tag you pinned.
|
||||
|
||||
Hit the health endpoint to confirm the API is responding:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8000/api/v1/health
|
||||
```
|
||||
|
||||
## Roll back
|
||||
|
||||
If something breaks, roll back by pinning the previous tag using the same process in **Option B** and restarting. Your Postgres data volume persists across `down`/`up` cycles, so agents and call history are preserved.
|
||||
|
||||
<Warning>
|
||||
Rolling back across a database migration is not always safe — if the newer release ran a schema migration, downgrading may leave the DB in a state the older API doesn't understand. If in doubt, [open an issue](https://github.com/dograh-hq/dograh/issues) before rolling back.
|
||||
</Warning>
|
||||
|
|
@ -141,6 +141,7 @@
|
|||
"deployment/introduction",
|
||||
"deployment/docker",
|
||||
"deployment/custom-domain",
|
||||
"deployment/update",
|
||||
"deployment/web-widget",
|
||||
"deployment/heroku"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -217,6 +217,15 @@ if ! alembic -c "$BASE_DIR/api/alembic.ini" upgrade head; then
|
|||
fi
|
||||
log_info "Migrations complete"
|
||||
|
||||
TS_VALIDATOR_DIR="$BASE_DIR/api/mcp_server/ts_validator"
|
||||
if [[ -f "$TS_VALIDATOR_DIR/package.json" ]]; then
|
||||
log_info "Installing ts_validator npm dependencies"
|
||||
if ! (cd "$TS_VALIDATOR_DIR" && npm install); then
|
||||
log_error "npm install for ts_validator failed. Aborting — nothing has been touched."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
### PHASE 2: START NEW WORKERS
|
||||
###############################################################################
|
||||
|
|
|
|||
|
|
@ -127,7 +127,16 @@ NGINX_UPSTREAM_TEMPLATE="$BASE_DIR/nginx/dograh_upstream.conf.template"
|
|||
NGINX_UPSTREAM_CONF="/etc/nginx/conf.d/dograh_upstream.conf"
|
||||
|
||||
###############################################################################
|
||||
### 4) Run migrations
|
||||
### 4) Install ts_validator npm dependencies
|
||||
###############################################################################
|
||||
|
||||
TS_VALIDATOR_DIR="$BASE_DIR/api/mcp_server/ts_validator"
|
||||
if [[ -f "$TS_VALIDATOR_DIR/package.json" ]]; then
|
||||
(cd "$TS_VALIDATOR_DIR" && npm install)
|
||||
fi
|
||||
|
||||
###############################################################################
|
||||
### 5) Run migrations
|
||||
###############################################################################
|
||||
|
||||
alembic -c "$BASE_DIR/api/alembic.ini" upgrade head
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ui",
|
||||
"version": "1.25.0",
|
||||
"version": "1.26.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS=--enable-source-maps next dev --turbopack",
|
||||
|
|
|
|||
|
|
@ -369,9 +369,10 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps)
|
|||
);
|
||||
|
||||
const isDirty = useMemo(() => {
|
||||
const d = data as unknown as Record<string, unknown>;
|
||||
return propertyNames.some((n) => values[n] !== d[n]);
|
||||
}, [values, data, propertyNames]);
|
||||
if (!spec) return false;
|
||||
const baseline = seedValues(data, spec);
|
||||
return propertyNames.some((n) => values[n] !== baseline[n]);
|
||||
}, [values, data, spec, propertyNames]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!spec) return;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import type { Team } from "@stackframe/stack";
|
||||
import {
|
||||
ArrowUpCircle,
|
||||
AudioLines,
|
||||
Brain,
|
||||
ChevronLeft,
|
||||
|
|
@ -54,6 +55,7 @@ import {
|
|||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
|
||||
import type { LocalUser } from "@/lib/auth";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -89,6 +91,12 @@ export function AppSidebar() {
|
|||
// Version info from app config context
|
||||
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
|
||||
|
||||
// Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user.
|
||||
const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion(
|
||||
versionInfo?.ui,
|
||||
{ enabled: config?.deploymentMode === "oss" },
|
||||
);
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return pathname.startsWith(path);
|
||||
};
|
||||
|
|
@ -232,17 +240,53 @@ export function AppSidebar() {
|
|||
<div className="flex items-center justify-between">
|
||||
{/* Logo - only show when expanded */}
|
||||
{effectiveState === "expanded" && (
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
||||
>
|
||||
Dograh
|
||||
{versionInfo && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
v{versionInfo.ui}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 px-2 text-xl font-bold"
|
||||
>
|
||||
Dograh
|
||||
{versionInfo && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
v{versionInfo.ui}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
{isBehind && latestRelease && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="https://docs.dograh.com/deployment/update"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
|
||||
>
|
||||
<ArrowUpCircle className="h-3 w-3" />
|
||||
Update
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>Latest: {latestRelease} — click to see the update guide</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</Link>
|
||||
{isLatest && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
|
||||
Latest
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>You're running the latest release</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Toggle button - center it when collapsed */}
|
||||
<SidebarTrigger className={cn(
|
||||
|
|
|
|||
107
ui/src/hooks/useLatestReleaseVersion.ts
Normal file
107
ui/src/hooks/useLatestReleaseVersion.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Options {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
latest: string | null;
|
||||
isBehind: boolean;
|
||||
isLatest: boolean;
|
||||
}
|
||||
|
||||
const CACHE_KEY = "dograh-latest-release";
|
||||
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
const SEMVER_RE = /^(?:[a-z][a-z0-9-]*-)?v?(\d+)\.(\d+)\.(\d+)$/i;
|
||||
|
||||
function parseSemver(tag: string): [number, number, number] | null {
|
||||
const m = tag.match(SEMVER_RE);
|
||||
if (!m) return null;
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||
}
|
||||
|
||||
function isOlder(current: string, latest: string): boolean {
|
||||
const c = parseSemver(current);
|
||||
const l = parseSemver(latest);
|
||||
if (!c || !l) return false;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (c[i] < l[i]) return true;
|
||||
if (c[i] > l[i]) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useLatestReleaseVersion(
|
||||
currentVersion: string | undefined,
|
||||
{ enabled }: Options,
|
||||
): Result {
|
||||
const [latest, setLatest] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !currentVersion) return;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(CACHE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as { tag: string; fetchedAt: number };
|
||||
if (Date.now() - parsed.fetchedAt < CACHE_TTL_MS) {
|
||||
setLatest(parsed.tag);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed cache
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest")
|
||||
.then((res) => (res.ok ? res.json() : null))
|
||||
.then((data) => {
|
||||
if (cancelled || !data?.tag_name) return;
|
||||
const tag = data.tag_name as string;
|
||||
try {
|
||||
localStorage.setItem(
|
||||
CACHE_KEY,
|
||||
JSON.stringify({ tag, fetchedAt: Date.now() }),
|
||||
);
|
||||
} catch {
|
||||
// storage may be full or disabled
|
||||
}
|
||||
setLatest(tag);
|
||||
})
|
||||
.catch(() => {
|
||||
// silent — don't break the sidebar if GitHub is unreachable
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [enabled, currentVersion]);
|
||||
|
||||
const normalizedCurrent = currentVersion
|
||||
? currentVersion.startsWith("v")
|
||||
? currentVersion
|
||||
: `v${currentVersion}`
|
||||
: null;
|
||||
|
||||
const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null;
|
||||
const latestParsed = latest ? parseSemver(latest) : null;
|
||||
|
||||
const isBehind = !!(
|
||||
normalizedCurrent &&
|
||||
latest &&
|
||||
isOlder(normalizedCurrent, latest)
|
||||
);
|
||||
|
||||
const isLatest = !!(
|
||||
currentParsed &&
|
||||
latestParsed &&
|
||||
currentParsed[0] === latestParsed[0] &&
|
||||
currentParsed[1] === latestParsed[1] &&
|
||||
currentParsed[2] === latestParsed[2]
|
||||
);
|
||||
|
||||
return { latest, isBehind, isLatest };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue