Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 391dd5ee80 | |||
| d8d44f0c6a | |||
| bf561186fb | |||
| 0afce741eb | |||
| 7603b739cb | |||
| ff8efdd887 | |||
| b289edf5a9 | |||
| 75b67f2a6f | |||
| bcc7486b59 | |||
| 29e765b2be | |||
| d14cdc1151 | |||
| c80dbf9737 |
@@ -1,150 +1,61 @@
|
|||||||
# Headquarter
|
# Headquarter
|
||||||
|
|
||||||
A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication.
|
Headquarter is a self-hosted workspace for managing projects, Git repositories, development-tool instances, SSH keys, and user preferences. It has a FastAPI API, a React frontend, PostgreSQL and Redis, and Authentik OAuth2/session authentication. Built-in tool definitions include code-server and Jupyter.
|
||||||
|
|
||||||
## Overview
|
## Prerequisites
|
||||||
|
|
||||||
Headquarter provides a centralized workspace for development teams to:
|
- Docker and Docker Compose for the provided local and production Compose stacks.
|
||||||
- Manage projects and their associated git repositories
|
- Git for repository workflows.
|
||||||
- Browse repository files and view git history
|
- Python 3.11 or newer for manual API development.
|
||||||
- Spawn development tools (VS Code Server, Jupyter Notebook, etc.)
|
- Node.js and npm for manual frontend development.
|
||||||
- Manage SSH keys and user preferences
|
- An Authentik configuration for the authenticated deployment.
|
||||||
|
|
||||||
## Features
|
Production additionally requires an existing Traefik network and host Docker access for tool-instance management.
|
||||||
|
|
||||||
### Project Management
|
## Local Compose setup
|
||||||
- Create and manage projects
|
|
||||||
- View all projects in a dashboard
|
|
||||||
- Click any project to open its workspace
|
|
||||||
|
|
||||||
### Git Repository Management
|
1. Create a local environment file from the template and replace placeholder credentials before using a shared or production-like environment:
|
||||||
- Initialize bare repositories
|
|
||||||
- Clone repositories (including mirror clones)
|
|
||||||
- Smart URL parsing (converts browser URLs to git URLs)
|
|
||||||
- View repository history and commit details
|
|
||||||
|
|
||||||
### Repository Workspace
|
|
||||||
- Browse files and directories
|
|
||||||
- View file contents with syntax highlighting
|
|
||||||
- Switch between branches
|
|
||||||
- Quick file editing with automatic commits
|
|
||||||
|
|
||||||
### Git History Visualization
|
|
||||||
- View commit history with branch graph
|
|
||||||
- See commit details, statistics, and diffs
|
|
||||||
- Filter by branch
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
- OAuth2 via Authentik
|
|
||||||
- Session-based authentication
|
|
||||||
- User profile management
|
|
||||||
|
|
||||||
### Tool Management
|
|
||||||
- Built-in tool types (code-server, jupyter-notebook)
|
|
||||||
- Create custom tool types with Docker Compose templates
|
|
||||||
- Template validation
|
|
||||||
|
|
||||||
### User Settings
|
|
||||||
- Theme selection (system/light/dark)
|
|
||||||
- Git identity configuration
|
|
||||||
- Default editor preference
|
|
||||||
|
|
||||||
### SSH Key Management
|
|
||||||
- Generate Ed25519 key pairs
|
|
||||||
- Copy public keys to clipboard
|
|
||||||
- Delete keys
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Docker and Docker Compose
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
1. **Clone the repository:**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd headquarter
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up environment:**
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your settings
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start services:**
|
2. Start the local stack:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Access the application:**
|
The local Compose stack starts PostgreSQL, Redis, the API, and the web frontend. It publishes the web frontend at <http://localhost:3000>, the API at <http://localhost:8000>, and API documentation at <http://localhost:8000/docs>. PostgreSQL and Redis are also published on ports 5432 and 6379 respectively.
|
||||||
- Frontend: http://localhost:5173
|
|
||||||
- API: http://localhost:8000
|
|
||||||
- API Docs: http://localhost:8000/docs
|
|
||||||
|
|
||||||
### Production Deployment
|
> **Authentication limitation:** this command does not configure the `AUTHENTIK_*`, `API_DOMAIN`, or `WEB_DOMAIN` values required for a verified authenticated flow. Treat authenticated local use as unsupported until those values are supplied through a documented local configuration.
|
||||||
|
|
||||||
See [Deployment Guide](docs/deployment/) for production setup with Traefik and Authentik.
|
Useful operational commands:
|
||||||
|
|
||||||
## Tech Stack
|
```bash
|
||||||
|
make up # Start services
|
||||||
### Backend
|
make down # Stop services
|
||||||
- **FastAPI** - Python web framework
|
make logs # Follow Compose logs
|
||||||
- **SQLAlchemy** - ORM with async PostgreSQL support
|
make migrate # Apply database migrations in the API container
|
||||||
- **Pydantic** - Data validation
|
make health # Show Compose service status
|
||||||
- **Alembic** - Database migrations
|
|
||||||
- **python-jose** - JWT handling
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **React** - UI library
|
|
||||||
- **TypeScript** - Type safety
|
|
||||||
- **Vite** - Build tool
|
|
||||||
- **React Router** - Client-side routing
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
- **Docker** - Containerization
|
|
||||||
- **PostgreSQL** - Database
|
|
||||||
- **Traefik** - Reverse proxy (production)
|
|
||||||
- **Authentik** - Identity provider
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- [User Guide](docs/features/) - Feature documentation
|
|
||||||
- [API Reference](docs/api/) - API endpoints
|
|
||||||
- [Architecture](docs/architecture/) - System design
|
|
||||||
- [Deployment](docs/deployment/) - Setup guides
|
|
||||||
- [Development](docs/development/) - Contributing
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── apps/
|
|
||||||
│ ├── api/ # FastAPI backend
|
|
||||||
│ │ ├── src/
|
|
||||||
│ │ │ ├── api/ # API routes
|
|
||||||
│ │ │ ├── auth/ # Authentication
|
|
||||||
│ │ │ ├── models/ # Database models
|
|
||||||
│ │ │ └── utils/ # Utilities
|
|
||||||
│ │ ├── tests/ # Test suite
|
|
||||||
│ │ └── Dockerfile
|
|
||||||
│ └── web/ # React frontend
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── api/ # API clients
|
|
||||||
│ │ ├── components/# UI components
|
|
||||||
│ │ └── pages/ # Page components
|
|
||||||
│ └── Dockerfile
|
|
||||||
├── docs/ # Documentation
|
|
||||||
├── docker-compose.yml # Development setup
|
|
||||||
├── docker-compose.traefik.yml # Production setup
|
|
||||||
└── Makefile # Common commands
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Production deployment
|
||||||
|
|
||||||
|
The production Compose file is [`docker-compose.traefik.yml`](docker-compose.traefik.yml). It expects an existing external Traefik network (named `traefik` by default), configured domains, an Authentik client secret, and host paths for repositories, working copies, and tool instances.
|
||||||
|
|
||||||
|
After preparing `.env` with production values, deploy with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.traefik.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The production API container mounts `/var/run/docker.sock` so it can manage tool instances. Treat this as privileged host access and restrict it appropriately. The deployment guide and supporting material are under [`docs/deployment/`](docs/deployment/).
|
||||||
|
|
||||||
|
## Manual development
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
### Backend Development
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/api
|
cd apps/api
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
@@ -153,43 +64,70 @@ pip install -e ".[dev]"
|
|||||||
uvicorn src.main:app --reload
|
uvicorn src.main:app --reload
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend Development
|
### Web
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/web
|
cd apps/web
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Tests
|
## Tests and quality checks
|
||||||
|
|
||||||
|
The Make targets run commands in the Compose services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Backend tests
|
|
||||||
make test
|
make test
|
||||||
|
make test-unit
|
||||||
# Frontend tests
|
make test-integration
|
||||||
make test-web
|
make test-system
|
||||||
|
make test-e2e
|
||||||
# All quality gates
|
|
||||||
make lint
|
make lint
|
||||||
make typecheck
|
make typecheck
|
||||||
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The frontend test command is run directly from its package directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/web
|
||||||
|
npm run test
|
||||||
|
```
|
||||||
|
|
||||||
|
`make lint` runs API Ruff and mypy checks plus frontend linting; `make typecheck` runs API mypy and frontend typechecking. Neither target runs frontend tests.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Key environment variables:
|
Copy [`.env.example`](.env.example) to `.env` and replace its placeholder values rather than committing credentials. Key settings include:
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Setting | Purpose |
|
||||||
|----------|-------------|---------|
|
| --- | --- |
|
||||||
| `API_DOMAIN` | API domain | `localhost` |
|
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | PostgreSQL connection settings |
|
||||||
| `WEB_DOMAIN` | Web domain | `localhost` |
|
| `REDIS_URL` | Redis connection URL |
|
||||||
| `AUTHENTIK_DOMAIN` | Authentik domain | - |
|
| `SESSION_SECRET`, `SESSION_TTL_HOURS` | Session signing and lifetime |
|
||||||
| `AUTHENTIK_CLIENT_ID` | OAuth client ID | - |
|
| `API_DOMAIN`, `WEB_DOMAIN` | Public API and web domains |
|
||||||
| `AUTHENTIK_CLIENT_SECRET` | OAuth client secret | - |
|
| `AUTHENTIK_DOMAIN`, `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET` | Authentik OAuth configuration |
|
||||||
| `DATABASE_URL` | PostgreSQL URL | - |
|
| `AUTHENTIK_APPLICATION_SLUG` | Authentik application path component |
|
||||||
| `JWT_SECRET` | JWT signing secret | - |
|
| `VITE_API_BASE_URL`, `VITE_APP_URL` | Frontend build-time public URLs |
|
||||||
| `REPO_BASE_PATH` | Repository storage path | `/data/repos` |
|
| `REPO_BASE_PATH` | Repository storage path |
|
||||||
|
| `TRAEFIK_NETWORK` | Existing Traefik network for the production Compose stack |
|
||||||
|
|
||||||
See [Environment Variables](docs/deployment/environment.md) for complete list.
|
The API reads `.env` settings and has development defaults, but defaults such as local credentials and session secrets are not suitable for production.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── apps/
|
||||||
|
│ ├── api/ # FastAPI API, migrations, and tests
|
||||||
|
│ └── web/ # React/Vite frontend
|
||||||
|
├── docs/ # Architecture, API, feature, deployment, and development docs
|
||||||
|
├── e2e/ # Playwright end-to-end tests
|
||||||
|
├── docker-compose.yml # Local Compose stack
|
||||||
|
├── docker-compose.traefik.yml # Traefik deployment stack
|
||||||
|
└── Makefile # Compose, test, quality, and build commands
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[License information]
|
No license file or declared license was found in this checkout.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getTerminalScrollbackLimit,
|
getTerminalScrollbackLimit,
|
||||||
isCurrentWebSocket,
|
isCurrentWebSocket,
|
||||||
shouldRetryWebSocketClose,
|
shouldRetryWebSocketClose,
|
||||||
|
shouldShowTerminalCopyMenu,
|
||||||
} from "./terminal.tsx";
|
} from "./terminal.tsx";
|
||||||
|
|
||||||
describe("getTerminalScrollbackLimit", () => {
|
describe("getTerminalScrollbackLimit", () => {
|
||||||
@@ -30,3 +31,13 @@ describe("getTerminalScrollbackLimit", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shouldShowTerminalCopyMenu", () => {
|
||||||
|
it("shows Copy for selected terminal output", () => {
|
||||||
|
expect(shouldShowTerminalCopyMenu("selected output")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the native context menu when no output is selected", () => {
|
||||||
|
expect(shouldShowTerminalCopyMenu("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -68,6 +68,22 @@ export function shouldRetryWebSocketClose(code: number, reason: string): boolean
|
|||||||
return code !== 1000 && !(code === 4000 && reason === "New connection established");
|
return code !== 1000 && !(code === 4000 && reason === "New connection established");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shouldShowTerminalCopyMenu(selection: string): boolean {
|
||||||
|
return selection.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTextWithFallback(text: string): void {
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute("readonly", "");
|
||||||
|
textarea.style.position = "fixed";
|
||||||
|
textarea.style.opacity = "0";
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
textarea.remove();
|
||||||
|
}
|
||||||
|
|
||||||
function matchesByteSequence(
|
function matchesByteSequence(
|
||||||
data: Uint8Array,
|
data: Uint8Array,
|
||||||
start: number,
|
start: number,
|
||||||
@@ -107,6 +123,11 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
>("connecting");
|
>("connecting");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||||
|
const [copyMenu, setCopyMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
text: string;
|
||||||
|
} | null>(null);
|
||||||
const activeModifierRef = useRef(activeModifier);
|
const activeModifierRef = useRef(activeModifier);
|
||||||
activeModifierRef.current = activeModifier;
|
activeModifierRef.current = activeModifier;
|
||||||
const [fontSize, setFontSize] = useState(() => {
|
const [fontSize, setFontSize] = useState(() => {
|
||||||
@@ -462,8 +483,27 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
term.paste(text);
|
term.paste(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBrowserCopy = (event: ClipboardEvent) => {
|
||||||
|
const selection = term.getSelection();
|
||||||
|
if (!selection) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.clipboardData?.setData("text/plain", selection);
|
||||||
|
};
|
||||||
|
const handleTerminalContextMenu = (event: MouseEvent) => {
|
||||||
|
const selection = term.getSelection();
|
||||||
|
if (!shouldShowTerminalCopyMenu(selection)) {
|
||||||
|
setCopyMenu(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
|
setCopyMenu({ x: event.clientX, y: event.clientY, text: selection });
|
||||||
|
};
|
||||||
|
|
||||||
const handleBrowserPaste = (event: ClipboardEvent) => {
|
const handleBrowserPaste = (event: ClipboardEvent) => {
|
||||||
if (!bracketedPasteEnabledRef.current) return;
|
|
||||||
const text = event.clipboardData?.getData("text/plain");
|
const text = event.clipboardData?.getData("text/plain");
|
||||||
if (text === undefined || wsRef.current?.readyState !== WebSocket.OPEN) {
|
if (text === undefined || wsRef.current?.readyState !== WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
@@ -473,6 +513,8 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
pasteTextRef.current(text);
|
pasteTextRef.current(text);
|
||||||
};
|
};
|
||||||
|
container.addEventListener("copy", handleBrowserCopy, true);
|
||||||
|
container.addEventListener("contextmenu", handleTerminalContextMenu, true);
|
||||||
container.addEventListener("paste", handleBrowserPaste, true);
|
container.addEventListener("paste", handleBrowserPaste, true);
|
||||||
|
|
||||||
// Mobile touch scroll.
|
// Mobile touch scroll.
|
||||||
@@ -720,6 +762,12 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
handleVisibilityChange,
|
handleVisibilityChange,
|
||||||
);
|
);
|
||||||
if (touchCleanup) touchCleanup();
|
if (touchCleanup) touchCleanup();
|
||||||
|
container.removeEventListener("copy", handleBrowserCopy, true);
|
||||||
|
container.removeEventListener(
|
||||||
|
"contextmenu",
|
||||||
|
handleTerminalContextMenu,
|
||||||
|
true,
|
||||||
|
);
|
||||||
container.removeEventListener("paste", handleBrowserPaste, true);
|
container.removeEventListener("paste", handleBrowserPaste, true);
|
||||||
pasteTextRef.current = () => {};
|
pasteTextRef.current = () => {};
|
||||||
bracketedPasteEnabledRef.current = false;
|
bracketedPasteEnabledRef.current = false;
|
||||||
@@ -854,6 +902,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
|
|
||||||
// Focus terminal on mobile to keep keyboard open
|
// Focus terminal on mobile to keep keyboard open
|
||||||
const handleTerminalClick = () => {
|
const handleTerminalClick = () => {
|
||||||
|
setCopyMenu(null);
|
||||||
if (isMobile && termRef.current) {
|
if (isMobile && termRef.current) {
|
||||||
termRef.current.focus();
|
termRef.current.focus();
|
||||||
}
|
}
|
||||||
@@ -968,6 +1017,26 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{copyMenu && (
|
||||||
|
<button
|
||||||
|
aria-label="Copy selected terminal text"
|
||||||
|
className="terminal-context-copy"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
copyTextWithFallback(copyMenu.text);
|
||||||
|
setCopyMenu(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
left: copyMenu.x,
|
||||||
|
position: "fixed",
|
||||||
|
top: copyMenu.y,
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="terminal-error">
|
<div className="terminal-error">
|
||||||
{error}
|
{error}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Fix Web Terminal Clipboard
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Users cannot reliably copy terminal output from the browser terminal. Browser copy shortcuts may be forwarded to the terminal as input instead of copying the xterm selection, and paste behavior differs by bracketed-paste mode.
|
||||||
|
|
||||||
|
## Required behavior
|
||||||
|
|
||||||
|
- Right-clicking a selected terminal region presents an xterm-aware Copy action that writes the selection to the system clipboard.
|
||||||
|
- Browser native context menus remain available when no terminal output is selected.
|
||||||
|
- Terminal keyboard input, including `Ctrl+C`, remains unchanged.
|
||||||
|
- Copy requests expose the xterm selection as plain text.
|
||||||
|
- Pasting plain text is handled once and follows bracketed-paste mode when enabled.
|
||||||
|
- Normal terminal interrupts still work when there is no active selection.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Web Terminal Clipboard Tasks
|
||||||
|
|
||||||
|
- [x] Add selected-text copy interception without suppressing unselected terminal interrupts.
|
||||||
|
- [x] Normalize browser paste handling through the existing paste transport.
|
||||||
|
- [x] Add focused frontend coverage for copy shortcut decisions.
|
||||||
|
- [x] Run frontend tests, typecheck/build, lint, and diagnostics.
|
||||||
Reference in New Issue
Block a user