Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 391dd5ee80 | |||
| d8d44f0c6a | |||
| bf561186fb | |||
| 0afce741eb | |||
| 7603b739cb | |||
| ff8efdd887 | |||
| b289edf5a9 | |||
| 75b67f2a6f | |||
| bcc7486b59 | |||
| 29e765b2be |
@@ -1,150 +1,61 @@
|
||||
# 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:
|
||||
- Manage projects and their associated git repositories
|
||||
- Browse repository files and view git history
|
||||
- Spawn development tools (VS Code Server, Jupyter Notebook, etc.)
|
||||
- Manage SSH keys and user preferences
|
||||
- Docker and Docker Compose for the provided local and production Compose stacks.
|
||||
- Git for repository workflows.
|
||||
- Python 3.11 or newer for manual API development.
|
||||
- Node.js and npm for manual frontend development.
|
||||
- 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
|
||||
- Create and manage projects
|
||||
- View all projects in a dashboard
|
||||
- Click any project to open its workspace
|
||||
## Local Compose setup
|
||||
|
||||
### Git Repository Management
|
||||
- Initialize bare repositories
|
||||
- Clone repositories (including mirror clones)
|
||||
- Smart URL parsing (converts browser URLs to git URLs)
|
||||
- View repository history and commit details
|
||||
1. Create a local environment file from the template and replace placeholder credentials before using a shared or production-like environment:
|
||||
|
||||
### 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
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
3. **Start services:**
|
||||
2. Start the local stack:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. **Access the application:**
|
||||
- Frontend: http://localhost:5173
|
||||
- API: http://localhost:8000
|
||||
- API Docs: http://localhost:8000/docs
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
### Backend
|
||||
- **FastAPI** - Python web framework
|
||||
- **SQLAlchemy** - ORM with async PostgreSQL support
|
||||
- **Pydantic** - Data validation
|
||||
- **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
|
||||
```bash
|
||||
make up # Start services
|
||||
make down # Stop services
|
||||
make logs # Follow Compose logs
|
||||
make migrate # Apply database migrations in the API container
|
||||
make health # Show Compose service status
|
||||
```
|
||||
|
||||
## 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
|
||||
cd apps/api
|
||||
python -m venv .venv
|
||||
@@ -153,43 +64,70 @@ pip install -e ".[dev]"
|
||||
uvicorn src.main:app --reload
|
||||
```
|
||||
|
||||
### Frontend Development
|
||||
### Web
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
## Tests and quality checks
|
||||
|
||||
The Make targets run commands in the Compose services:
|
||||
|
||||
```bash
|
||||
# Backend tests
|
||||
make test
|
||||
|
||||
# Frontend tests
|
||||
make test-web
|
||||
|
||||
# All quality gates
|
||||
make test-unit
|
||||
make test-integration
|
||||
make test-system
|
||||
make test-e2e
|
||||
make lint
|
||||
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
|
||||
|
||||
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 |
|
||||
|----------|-------------|---------|
|
||||
| `API_DOMAIN` | API domain | `localhost` |
|
||||
| `WEB_DOMAIN` | Web domain | `localhost` |
|
||||
| `AUTHENTIK_DOMAIN` | Authentik domain | - |
|
||||
| `AUTHENTIK_CLIENT_ID` | OAuth client ID | - |
|
||||
| `AUTHENTIK_CLIENT_SECRET` | OAuth client secret | - |
|
||||
| `DATABASE_URL` | PostgreSQL URL | - |
|
||||
| `JWT_SECRET` | JWT signing secret | - |
|
||||
| `REPO_BASE_PATH` | Repository storage path | `/data/repos` |
|
||||
| Setting | Purpose |
|
||||
| --- | --- |
|
||||
| `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | PostgreSQL connection settings |
|
||||
| `REDIS_URL` | Redis connection URL |
|
||||
| `SESSION_SECRET`, `SESSION_TTL_HOURS` | Session signing and lifetime |
|
||||
| `API_DOMAIN`, `WEB_DOMAIN` | Public API and web domains |
|
||||
| `AUTHENTIK_DOMAIN`, `AUTHENTIK_CLIENT_ID`, `AUTHENTIK_CLIENT_SECRET` | Authentik OAuth configuration |
|
||||
| `AUTHENTIK_APPLICATION_SLUG` | Authentik application path component |
|
||||
| `VITE_API_BASE_URL`, `VITE_APP_URL` | Frontend build-time public URLs |
|
||||
| `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 information]
|
||||
No license file or declared license was found in this checkout.
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getTerminalScrollbackLimit,
|
||||
isCurrentWebSocket,
|
||||
shouldRetryWebSocketClose,
|
||||
shouldCopyTerminalSelection,
|
||||
shouldShowTerminalCopyMenu,
|
||||
} from "./terminal.tsx";
|
||||
|
||||
describe("getTerminalScrollbackLimit", () => {
|
||||
@@ -32,28 +32,12 @@ describe("getTerminalScrollbackLimit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldCopyTerminalSelection", () => {
|
||||
it("copies a selected terminal region with Ctrl+C or Cmd+C", () => {
|
||||
expect(
|
||||
shouldCopyTerminalSelection(
|
||||
{ ctrlKey: true, metaKey: false, key: "c" },
|
||||
true,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldCopyTerminalSelection(
|
||||
{ ctrlKey: false, metaKey: true, key: "C" },
|
||||
true,
|
||||
),
|
||||
).toBe(true);
|
||||
describe("shouldShowTerminalCopyMenu", () => {
|
||||
it("shows Copy for selected terminal output", () => {
|
||||
expect(shouldShowTerminalCopyMenu("selected output")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps Ctrl+C as a terminal interrupt without a selection", () => {
|
||||
expect(
|
||||
shouldCopyTerminalSelection(
|
||||
{ ctrlKey: true, metaKey: false, key: "c" },
|
||||
false,
|
||||
),
|
||||
).toBe(false);
|
||||
it("keeps the native context menu when no output is selected", () => {
|
||||
expect(shouldShowTerminalCopyMenu("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,15 +68,20 @@ export function shouldRetryWebSocketClose(code: number, reason: string): boolean
|
||||
return code !== 1000 && !(code === 4000 && reason === "New connection established");
|
||||
}
|
||||
|
||||
export function shouldCopyTerminalSelection(
|
||||
event: Pick<KeyboardEvent, "ctrlKey" | "metaKey" | "key">,
|
||||
hasSelection: boolean,
|
||||
): boolean {
|
||||
return (
|
||||
hasSelection &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key.toLowerCase() === "c"
|
||||
);
|
||||
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(
|
||||
@@ -118,6 +123,11 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
>("connecting");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showResetConfirm, setShowResetConfirm] = useState(false);
|
||||
const [copyMenu, setCopyMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
text: string;
|
||||
} | null>(null);
|
||||
const activeModifierRef = useRef(activeModifier);
|
||||
activeModifierRef.current = activeModifier;
|
||||
const [fontSize, setFontSize] = useState(() => {
|
||||
@@ -480,27 +490,18 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
event.preventDefault();
|
||||
event.clipboardData?.setData("text/plain", selection);
|
||||
};
|
||||
const copySelection = () => {
|
||||
const handleTerminalContextMenu = (event: MouseEvent) => {
|
||||
const selection = term.getSelection();
|
||||
if (!selection) return;
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard) {
|
||||
void clipboard.writeText(selection).catch(() => {
|
||||
document.execCommand("copy");
|
||||
});
|
||||
} else {
|
||||
document.execCommand("copy");
|
||||
}
|
||||
};
|
||||
term.attachCustomKeyEventHandler((event) => {
|
||||
if (!shouldCopyTerminalSelection(event, term.hasSelection())) {
|
||||
return true;
|
||||
if (!shouldShowTerminalCopyMenu(selection)) {
|
||||
setCopyMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
copySelection();
|
||||
return false;
|
||||
});
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
setCopyMenu({ x: event.clientX, y: event.clientY, text: selection });
|
||||
};
|
||||
|
||||
const handleBrowserPaste = (event: ClipboardEvent) => {
|
||||
const text = event.clipboardData?.getData("text/plain");
|
||||
@@ -513,6 +514,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
pasteTextRef.current(text);
|
||||
};
|
||||
container.addEventListener("copy", handleBrowserCopy, true);
|
||||
container.addEventListener("contextmenu", handleTerminalContextMenu, true);
|
||||
container.addEventListener("paste", handleBrowserPaste, true);
|
||||
|
||||
// Mobile touch scroll.
|
||||
@@ -761,6 +763,11 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
);
|
||||
if (touchCleanup) touchCleanup();
|
||||
container.removeEventListener("copy", handleBrowserCopy, true);
|
||||
container.removeEventListener(
|
||||
"contextmenu",
|
||||
handleTerminalContextMenu,
|
||||
true,
|
||||
);
|
||||
container.removeEventListener("paste", handleBrowserPaste, true);
|
||||
pasteTextRef.current = () => {};
|
||||
bracketedPasteEnabledRef.current = false;
|
||||
@@ -895,6 +902,7 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
|
||||
// Focus terminal on mobile to keep keyboard open
|
||||
const handleTerminalClick = () => {
|
||||
setCopyMenu(null);
|
||||
if (isMobile && termRef.current) {
|
||||
termRef.current.focus();
|
||||
}
|
||||
@@ -1009,6 +1017,26 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
</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 && (
|
||||
<div className="terminal-error">
|
||||
{error}
|
||||
|
||||
@@ -6,7 +6,9 @@ Users cannot reliably copy terminal output from the browser terminal. Browser co
|
||||
|
||||
## Required behavior
|
||||
|
||||
- Copying a selected terminal region with standard browser shortcuts (`Ctrl/Cmd+C`) copies text to the system clipboard without sending an interrupt to the terminal.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user