Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/index.html ./index.html

RUN mkdir -p /home/node/.gh-issues-dashboard \
&& chown -R node:node /home/node/.gh-issues-dashboard /app
RUN mkdir -p /home/node/.gitdeck \
&& chown -R node:node /home/node/.gitdeck /app

USER node

EXPOSE 8765
VOLUME ["/home/node/.gh-issues-dashboard"]
VOLUME ["/home/node/.gitdeck"]

CMD ["node", "dist/server.js"]
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# gh-dashboard

> `gh-dashboard` is a working name. The final project name will be picked together with the community — share your suggestion in the [naming discussion](https://github.com/debba/gh-dashboard/discussions/1) or on Discord.
# Gitdeck

> The initial scaffolding of this repository was produced in an AI-assisted session with [Claude Code](https://claude.com/claude-code). From here on, code is reviewed and maintained by humans, and contributions are welcome.

Expand All @@ -12,12 +10,12 @@
<a href="https://repostars.dev/?repos=debba%2Fgh-dashboard&theme=dark"><img src="https://repostars.dev/api/embed?repo=debba%2Fgh-dashboard&theme=dark" alt="RepoStars" /></a>
</p>

An open-source dashboard to explore your GitHub repositories, issues, pull requests, traffic, and CI activity from a single interface.
An open-source, local dashboard to explore repositories, issues, pull requests, traffic, and CI activity across multiple accounts on GitHub and Forgejo-compatible forges (Codeberg, self-hosted) — from a single interface.

## Demo

<div align="center">
<img src="public/demo.gif" alt="gh-dashboard demo" />
<img src="public/demo.gif" alt="Gitdeck demo" />
</div>

## What it does
Expand Down Expand Up @@ -49,7 +47,7 @@ Open any repository to see:

The app is a single repository with two cooperating processes:

- **Backend** — a Node HTTP server (`src/server.ts` + `src/server/*`) that handles GitHub OAuth (Device Flow), proxies all REST/GraphQL calls, caches responses on disk, and exposes a small JSON API under `/api/*`. The GitHub token is stored locally under `~/.gh-issues-dashboard/` and **never exposed to the browser**.
- **Backend** — a Node HTTP server (`src/server.ts` + `src/server/*`) that handles GitHub OAuth (Device Flow), proxies all REST/GraphQL calls, caches responses on disk, and exposes a small JSON API under `/api/*`. The GitHub token is stored locally under `~/.gitdeck/` and **never exposed to the browser**.
- **Frontend** — a React 19 + Vite SPA (`src/main.tsx`, `src/App.tsx`, `src/components/*`, `src/api/*`) that consumes the backend's `/api/*` endpoints.

In production both are served by the Node process: Vite builds the SPA into `dist/client/` and the server falls back to `index.html` for non-API routes.
Expand Down Expand Up @@ -83,7 +81,7 @@ The dashboard talks to GitHub using a personal **OAuth App** with the **Device A
1. Go to <https://github.com/settings/developers> → **OAuth Apps** → **New OAuth App**.
(For an org-owned app, use **Settings → Developer settings → OAuth Apps** on the organization instead.)
2. Fill in the form:
- **Application name** — anything, e.g. `gh-dashboard (local)`.
- **Application name** — anything, e.g. `Gitdeck (local)`.
- **Homepage URL** — `http://127.0.0.1:8765` (or any URL you control; this is informational).
- **Authorization callback URL** — `http://127.0.0.1:8765` will do. Device Flow does not actually use a redirect, but GitHub requires the field.
3. Click **Register application**.
Expand Down Expand Up @@ -111,9 +109,9 @@ When you open the dashboard for the first time, it will:
1. call the backend, which asks GitHub for a **device code**;
2. show you a short **user code** and a verification URL (typically <https://github.com/login/device>);
3. you paste the code on GitHub and approve the requested scopes;
4. the backend exchanges the device code for an access token and stores it in `~/.gh-issues-dashboard/` — **the token never reaches the browser**.
4. the backend exchanges the device code for an access token and stores it in `~/.gitdeck/` — **the token never reaches the browser**.

Granted scopes default to `repo read:org project read:user user:email`. To narrow them, set `GITHUB_OAUTH_SCOPES` (see [Configuration](#configuration)). If you ever want to revoke access, remove the app from <https://github.com/settings/applications> and delete the local token file under `~/.gh-issues-dashboard/`.
Granted scopes default to `repo read:org project read:user user:email`. To narrow them, set `GITHUB_OAUTH_SCOPES` (see [Configuration](#configuration)). If you ever want to revoke access, remove the app from <https://github.com/settings/applications> and delete the local token file under `~/.gitdeck/`.

## Configuration

Expand All @@ -134,13 +132,13 @@ The server reads its configuration from environment variables:

The dashboard can obtain a GitHub token in three different ways. Pick the one that fits your setup:

- **`device` (default)** — OAuth App + Device Flow, as described above. The token is stored under `~/.gh-issues-dashboard/` and refreshed via the in-app sign-in screen. Requires `GITHUB_CLIENT_ID`.
- **`device` (default)** — OAuth App + Device Flow, as described above. The token is stored under `~/.gitdeck/` and refreshed via the in-app sign-in screen. Requires `GITHUB_CLIENT_ID`.
- **`gh-cli`** — if you already use the [GitHub CLI](https://cli.github.com/), set `GH_AUTH_MODE=gh-cli` and the server will read the token by running `gh auth token` on each request (cached in-process for 60s). No OAuth App is needed; the scopes are whatever your `gh` session already has. Run `gh auth refresh -h github.com -s repo,read:org,project` if you need extra scopes.
- **`token`** — bring-your-own personal access token. Set `GH_AUTH_MODE=token` and export `GITHUB_TOKEN=<your-pat>`. Useful for headless / CI-style deployments.

In `gh-cli` and `token` modes the device-flow sign-in screen is hidden; the server treats the configured source as authoritative.

Tokens and snapshots are persisted under `~/.gh-issues-dashboard/`.
Tokens and snapshots are persisted under `~/.gitdeck/`. If you previously ran an older build that stored data in `~/.gh-issues-dashboard/`, the server migrates it automatically on first start.

### Quick env setup

Expand Down Expand Up @@ -196,7 +194,7 @@ Then open <http://127.0.0.1:8765>.

## Run with Docker

A multi-stage `Dockerfile` and a `docker-compose.yml` are provided. The image builds the server + SPA bundle and runs as a non-root user; tokens and snapshots are persisted to a named volume mounted at `/home/node/.gh-issues-dashboard`.
A multi-stage `Dockerfile` and a `docker-compose.yml` are provided. The image builds the server + SPA bundle and runs as a non-root user; tokens and snapshots are persisted to a named volume mounted at `/home/node/.gitdeck`.

With Docker Compose (recommended):

Expand All @@ -216,16 +214,16 @@ docker compose up -d --build
With plain Docker:

```bash
docker build -t gh-dashboard .
docker run -d --name gh-dashboard \
docker build -t gitdeck .
docker run -d --name gitdeck \
-p 8765:8765 \
-e GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx \
-e OPENAI_API_KEY=sk-... \
-v gh-dashboard-data:/home/node/.gh-issues-dashboard \
gh-dashboard
-v gitdeck-data:/home/node/.gitdeck \
gitdeck
```

The container forwards `GITHUB_CLIENT_ID`, `GITHUB_OAUTH_SCOPES`, `OPENAI_API_KEY` and `OPENAI_DIGEST_MODEL` from the host environment (or `.env` with Compose) — see [Configuration](#configuration) for the full list. It sets `HOST=0.0.0.0` so the server is reachable from outside. To wipe the stored token (full logout) remove the volume: `docker volume rm gh-dashboard-data`.
The container forwards `GITHUB_CLIENT_ID`, `GITHUB_OAUTH_SCOPES`, `OPENAI_API_KEY` and `OPENAI_DIGEST_MODEL` from the host environment (or `.env` with Compose) — see [Configuration](#configuration) for the full list. It sets `HOST=0.0.0.0` so the server is reachable from outside. To wipe the stored token (full logout) remove the volume: `docker volume rm gitdeck-data`.

## Test & type-check

Expand Down Expand Up @@ -266,7 +264,6 @@ Early scaffolding. APIs, modules, and the UI are still being shaped — expect r
## Community

- [Discord server](https://discord.gg/YrZPHAwMSG) — suggest features, report issues, or just say hi.
- [Help name the project](https://github.com/debba/gh-dashboard/discussions/1) — open discussion for naming suggestions.

## Contributing

Expand Down
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
services:
gh-dashboard:
gitdeck:
build: .
image: gh-dashboard:latest
container_name: gh-dashboard
image: gitdeck:latest
container_name: gitdeck
restart: unless-stopped
ports:
- "8765:8765"
Expand All @@ -12,7 +12,7 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_DIGEST_MODEL: ${OPENAI_DIGEST_MODEL:-}
volumes:
- gh-dashboard-data:/home/node/.gh-issues-dashboard
- gitdeck-data:/home/node/.gitdeck

volumes:
gh-dashboard-data:
gitdeck-data:
2 changes: 1 addition & 1 deletion docs/translations.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Use a lowercase language code as the file name. For example, Portuguese would us
import type { en } from "./en";

export const pt: Record<keyof typeof en, string> = {
"app.title": "GitHub Dashboard",
"app.title": "Gitdeck",
// ...
};
```
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<title>GitHub Dashboard</title>
<title>Gitdeck</title>
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "gh-issues-dashboard",
"name": "gitdeck",
"version": "1.0.2",
"private": true,
"type": "module",
"description": "Local dashboard showing open issues across your GitHub repositories using GitHub OAuth.",
"description": "Local multi-account dashboard for GitHub and Forgejo-compatible forges (Codeberg, self-hosted).",
"scripts": {
"api": "tsx watch src/server.ts",
"dev": "concurrently \"npm:api\" \"vite --host 127.0.0.1\"",
Expand Down
8 changes: 6 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { formatNumber } from "./utils/format";
import { clearStatsCache, readStatsCache, writeStatsCache } from "./utils/statsCache";
import { clearFiltersCache, hydrateFilters, readFiltersCache, writeFiltersCache } from "./utils/filtersCache";
import { useI18n } from "./i18n/I18nProvider";
import { useCapability } from "./contexts/AccountContext";

type Tab = "inbox" | "repos" | "issues" | "prs" | "kanban" | "insights" | "ci" | "digests";
type Theme = "dark" | "light" | "auto";
Expand Down Expand Up @@ -159,6 +160,7 @@ type AuthState = "checking" | "anonymous" | "authenticated";

export function App() {
const { t } = useI18n();
const projectsEnabled = useCapability("projects");
const location = useLocation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
Expand Down Expand Up @@ -657,7 +659,9 @@ export function App() {
{ key: "insights" as const, label: t("tabs.insights"), count: filteredInsights.length, icon: <PulseIcon /> },
{ key: "ci" as const, label: t("tabs.ci"), count: ciHealth.length, icon: <PulseIcon /> },
{ key: "digests" as const, label: t("tabs.digest"), count: dailyDigests.length, icon: <PulseIcon /> },
{ key: "kanban" as const, label: t("tabs.board"), count: "—", icon: <BoardIcon /> },
...(projectsEnabled
? [{ key: "kanban" as const, label: t("tabs.board"), count: "—", icon: <BoardIcon /> }]
: []),
];

return (
Expand Down Expand Up @@ -885,7 +889,7 @@ export function App() {
</div>
) : null}

{tab === "kanban" ? <KanbanView /> : null}
{tab === "kanban" && projectsEnabled ? <KanbanView /> : null}
</main>
</div>
<Footer
Expand Down
65 changes: 65 additions & 0 deletions src/api/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,71 @@ export function logoutAuth(): Promise<{ ok: true }> {
return readJson<{ ok: true }>("/api/auth/logout", { method: "POST" });
}

export interface AccountSummary {
id: string;
providerKind: "github" | "forgejo";
providerConfigId: string;
label: string;
login: string | null;
scope: string;
source: "device" | "gh-cli" | "token" | "env";
ephemeral: boolean;
active: boolean;
capabilities: {
graphql?: boolean;
notifications?: boolean;
projects?: boolean;
ciWorkflows?: boolean;
codeSearch?: boolean;
dependents?: boolean;
traffic?: boolean;
stargazerHistory?: boolean;
};
}

export interface AccountsList {
ok: true;
accounts: AccountSummary[];
activeId: string | null;
}

export function fetchAccounts(): Promise<AccountsList> {
return readJson<AccountsList>("/api/accounts");
}

export function activateAccount(id: string): Promise<{ ok: true; activeId: string }> {
return readJson("/api/accounts/activate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id }),
});
}

export function removeAccount(id: string): Promise<{ ok: true }> {
const query = new URLSearchParams({ id });
return readJson(`/api/accounts?${query.toString()}`, { method: "DELETE" });
}

export interface ProviderConfigSummary {
id: string;
kind: "github" | "forgejo";
label: string;
webUrl: string;
supportsDeviceFlow: boolean;
}

export function fetchProviderConfigs(): Promise<{ ok: true; configs: ProviderConfigSummary[] }> {
return readJson<{ ok: true; configs: ProviderConfigSummary[] }>("/api/provider-configs");
}

export function addTokenAccount(payload: { providerConfigId: string; token: string; label?: string }): Promise<{ ok: true; accountId: string }> {
return readJson("/api/accounts/add-token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}

export function fetchRepos(fresh = false, signal?: AbortSignal): Promise<ReposData> {
return readJson<ReposData>(`/api/repos${fresh ? "?fresh=1" : ""}`, withSignal(signal), "/api/repos");
}
Expand Down
Loading