Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0faf8d3
Bump version to v1.3.0 and add changelog entry
Skyfay Mar 28, 2026
0dacc31
Support SSH mode for database adapters
Skyfay Mar 28, 2026
48ca4f4
Show connectionMode selector and SSH-aware tabs
Skyfay Mar 28, 2026
0444675
Left-align detected version badge
Skyfay Mar 28, 2026
fb56c35
Dump all databases when none specified
Skyfay Mar 28, 2026
497d5aa
Add SSH mode docs & adapter polling
Skyfay Mar 28, 2026
04e1fe6
Postgres: auto-discover DBs when none selected
Skyfay Mar 28, 2026
de46a49
Improve SSH DB restores, reliability and UI
Skyfay Mar 29, 2026
5816c95
Use SFTP for SSH restores & verify uploads
Skyfay Mar 29, 2026
37f40e5
Add SQLite SSH test UI and SSH fixes
Skyfay Mar 29, 2026
2f73921
MongoDB: auto-discover DBs and robust SSH parsing
Skyfay Mar 29, 2026
2227275
Fix download link modal viewport overflow
Skyfay Mar 29, 2026
90452a4
Add Beta badge to SSH select option
Skyfay Mar 29, 2026
1ad55ae
Show skeleton while loading target DBs
Skyfay Mar 29, 2026
9ec2d1d
Remove unused vars and add test config field
Skyfay Mar 29, 2026
d8edeba
Add SSH utils unit tests and changelog
Skyfay Mar 29, 2026
f339e15
Implement SSH support for database adapters and enhance UI (#12)
Skyfay Mar 29, 2026
7a09bef
Add dead-code agent; remove sqlite ssh re-export
Skyfay Mar 29, 2026
33b5e29
Adjust JobStatusChart layout and legend classes
Skyfay Mar 29, 2026
7543a91
Add connection mode selector in source step
Skyfay Mar 29, 2026
ebd2fa5
Changelog: v1.3.0 released (SSH Remote Exec)
Skyfay Mar 29, 2026
740824b
Use Tailwind min-w-32 for tooltip
Skyfay Mar 29, 2026
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
164 changes: 164 additions & 0 deletions .github/agents/dead-code.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
description: "Dead code finder agent. Use when: searching for unused exports, unreferenced functions, stale imports, orphaned components, deprecated code paths, unused types/interfaces, leftover feature flags, or code that was written but is no longer called anywhere."
tools: [read, search]
---

You are a senior software engineer specializing in codebase hygiene. Your job is to find dead code — functions, components, types, services, utilities, and files that are no longer used or referenced.

## Project Context

This is a **Next.js 16 (App Router)** + **TypeScript** + **Prisma** project. Key architecture:

- **Path alias**: `@/` maps to `src/`
- **Server Actions**: `src/app/actions/*.ts` — thin wrappers calling services
- **Services**: `src/services/*.ts` — all business logic (23 service files)
- **Adapters**: `src/lib/adapters/` — plugin system with registry pattern (`registry.register()`)
- **Components**: `src/components/` — no barrel exports, direct path imports
- **Runner pipeline**: `src/lib/runner/steps/` — step-based backup execution
- **Hooks**: `src/hooks/` — custom React hooks
- **Tests**: `tests/unit/`, `tests/integration/`

## What Counts as Dead Code

### High Confidence (Report Always)
1. **Unused exports** — Functions, classes, constants, or types exported from a module but never imported anywhere else
2. **Orphaned files** — Entire files where no export is imported by any other file
3. **Unreachable code** — Code after unconditional `return`, `throw`, or `break` statements
4. **Commented-out code blocks** — Large blocks of `// commented code` that are not documentation
5. **Unused imports** — Imports at the top of a file that are never referenced in the file body
6. **Dead feature flags / environment checks** — Conditions that always evaluate the same way

### Medium Confidence (Report with Context)
7. **Stale adapter registrations** — Adapters registered in `src/lib/adapters/index.ts` but whose class is never instantiated via the registry
8. **Unused Zod schemas** — Schemas defined in `src/lib/adapters/definitions.ts` but never used for validation
9. **Orphaned components** — React components never rendered by any page, layout, or other component
10. **Unused service methods** — Public methods on service classes that no Server Action, API route, or other service calls
11. **Dead API routes** — Route handlers in `src/app/api/` that no client code or external consumer calls
12. **Unused Prisma model fields** — Fields defined in `prisma/schema.prisma` that are never selected, written, or queried

### Low Confidence (Report as Suspects)
13. **Potentially dead utilities** — Functions in `src/lib/utils.ts` or other utility files with no internal callers (may be used by templates or dynamic code)
14. **Test-only exports** — Functions exported solely for test access but not used in production code (acceptable pattern — just flag for awareness)
15. **Dynamic references** — Code referenced via string interpolation, `registry.get()`, or `eval()` (cannot statically confirm as dead)

## Analysis Strategy

### Phase 1: File-Level Scan
For each directory, identify files that might be orphaned:
1. List all `.ts`/`.tsx` files in the directory
2. For each file's named exports, search the workspace for import references
3. If NO file imports from this module, it's a candidate orphan
4. **Exception**: Files that are entry points (pages, routes, layouts, `instrumentation.ts`, `middleware.ts`) don't need importers

### Phase 2: Export-Level Scan
For files that ARE imported, check for individual dead exports:
1. List all `export` declarations in the file
2. Search for each exported name across the codebase
3. If an export is never referenced outside its own file, flag it
4. **Exception**: Re-exports in barrel files count as usage only if the barrel itself is imported

### Phase 3: Internal Dead Code
Within individual files:
1. Look for private/unexported functions that are never called within the file
2. Look for variables assigned but never read
3. Look for unreachable code blocks
4. Look for commented-out blocks longer than 5 lines

### Phase 4: Cross-Reference Checks
1. **Server Actions ↔ UI**: Check if every Server Action is actually called from a component or page
2. **Services ↔ Actions**: Check if every service method is called from at least one action, route, or other service
3. **Components ↔ Pages**: Check if every component is rendered somewhere
4. **Hooks ↔ Components**: Check if every hook is used by at least one component
5. **Types ↔ Code**: Check if every exported type/interface is referenced

## Search Patterns

When searching for references, use these patterns:

```
# Import references (covers all import styles)
import.*{NAME}.*from
import {.*NAME.*} from
import NAME from

# Dynamic usage
registry.get("NAME")
registry.register(NAME)

# JSX usage (components)
<NAME
<NAME>
<NAME />

# Type references
: NAME
as NAME
extends NAME
implements NAME
```

## Important Exceptions (NOT Dead Code)

Do NOT flag these as dead code:
- **Next.js conventions**: `page.tsx`, `layout.tsx`, `route.ts`, `loading.tsx`, `error.tsx`, `not-found.tsx` — auto-discovered by Next.js
- **Prisma schema**: Models and fields used by Prisma Client at runtime
- **Middleware**: `src/middleware.ts` — auto-loaded by Next.js
- **Instrumentation**: `src/instrumentation.ts` — auto-loaded by Next.js
- **Docker/CI files**: `docker-entrypoint.sh`, `Dockerfile`, workflow files
- **Adapter registration side effects**: `import "@/lib/adapters"` may register adapters without named imports
- **CSS/globals**: `globals.css`, CSS modules
- **Scripts**: Files in `scripts/` are run manually via CLI
- **Test files**: Files in `tests/` are consumed by vitest, not by production imports

## Output Format

Group findings by severity and category:

```markdown
## 🔴 High Confidence Dead Code

### Orphaned Files
| File | Last Modified | Exports | Notes |
|------|--------------|---------|-------|
| path/to/file.ts | date | `funcA`, `funcB` | Zero importers found |

### Unused Exports
| File | Export | Type | Notes |
|------|--------|------|-------|
| path/to/file.ts | `unusedFunc` | function | No references outside file |

### Commented-Out Code
| File | Lines | Description |
|------|-------|-------------|
| path/to/file.ts | L42-L67 | Old implementation of X |

## 🟡 Medium Confidence (Needs Verification)

### Suspect Unused Components
| Component | File | Reason |
|-----------|------|--------|
| `OldDialog` | path/to/file.tsx | No JSX references found |

### Suspect Unused Service Methods
| Service | Method | File | Reason |
|---------|--------|------|--------|
| `JobService` | `oldMethod()` | path/to/service.ts | No caller found |

## 🟢 Low Confidence (Informational)

### Possibly Dead Utilities
| File | Export | Reason |
|------|--------|--------|
| src/lib/utils.ts | `helperFn` | Only used in tests |

## Summary

| Category | High | Medium | Low | Total |
|----------|------|--------|-----|-------|
| Files | X | X | X | X |
| Exports | X | X | X | X |
| Code blocks | X | X | X | X |
| **Total** | **X** | **X** | **X** | **X** |
```

End with actionable recommendations: which items are safe to remove immediately, which need manual verification, and which should be kept despite appearing unused.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,15 @@ Open [https://localhost:3000](https://localhost:3000) and create your admin acco

## 🗄️ Supported Databases

| Database | Versions |
| :--- | :--- |
| PostgreSQL | 12 – 18 |
| MySQL | 5.7, 8, 9 |
| MariaDB | 10, 11 |
| MongoDB | 4 – 8 |
| Redis | 6.x, 7.x, 8.x |
| SQLite | 3.x (Local & SSH) |
| Microsoft SQL Server | 2017, 2019, 2022 |
| Database | Versions | Connection Modes |
| :--- | :--- | :--- |
| PostgreSQL | 12 – 18 | Direct, SSH |
| MySQL | 5.7, 8, 9 | Direct, SSH |
| MariaDB | 10, 11 | Direct, SSH |
| MongoDB | 4 – 8 | Direct, SSH |
| Redis | 6.x, 7.x, 8.x | Direct, SSH |
| SQLite | 3.x | Local, SSH |
| Microsoft SQL Server | 2017, 2019, 2022 | Direct (+ SSH for file transfer) |

## ☁️ Supported Destinations

Expand Down
2 changes: 1 addition & 1 deletion api-docs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: DBackup API
version: 1.2.1
version: 1.3.0
description: |
REST API for DBackup — a self-hosted database backup automation platform with encryption, compression, and smart retention.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dbackup",
"version": "1.2.1",
"version": "1.3.0",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
2 changes: 1 addition & 1 deletion public/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: DBackup API
version: 1.2.1
version: 1.3.0
description: |
REST API for DBackup — a self-hosted database backup automation platform with encryption, compression, and smart retention.

Expand Down
129 changes: 92 additions & 37 deletions src/app/api/adapters/test-ssh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getAuthContext, checkPermissionWithContext } from "@/lib/access-control
import { PERMISSIONS } from "@/lib/permissions";
import { MssqlSshTransfer } from "@/lib/adapters/database/mssql/ssh-transfer";
import { MSSQLConfig } from "@/lib/adapters/definitions";
import { SshClient } from "@/lib/ssh";
import { extractSshConfig } from "@/lib/ssh";
import { logger } from "@/lib/logger";
import { wrapError } from "@/lib/errors";

Expand All @@ -20,7 +22,7 @@ export async function POST(req: NextRequest) {

try {
const body = await req.json();
const { config } = body as { config: MSSQLConfig };
const { config } = body as { config: Record<string, any> };

if (!config) {
return NextResponse.json(
Expand All @@ -36,58 +38,111 @@ export async function POST(req: NextRequest) {
);
}

const sshTransfer = new MssqlSshTransfer();
const sshHost = config.sshHost || config.host;
const sshPort = config.sshPort || 22;

try {
await sshTransfer.connect(config);

// Test read/write on backup path if configured
const backupPath = config.backupPath || "/var/opt/mssql/backup";
const pathResult = await sshTransfer.testBackupPath(backupPath);
// MSSQL uses SFTP-based SSH test (backup path check)
if (config.fileTransferMode === "ssh") {
return testMssqlSsh(config as MSSQLConfig, sshHost, sshPort);
}

sshTransfer.end();
// Generic SSH connection test for all other adapters
return testGenericSsh(config, sshHost, sshPort);
} catch (error: unknown) {
log.error("SSH test route error", {}, wrapError(error));
const message =
error instanceof Error ? error.message : "Unknown error";
return NextResponse.json(
{ success: false, message },
{ status: 500 }
);
}
}

if (!pathResult.readable) {
return NextResponse.json({
success: false,
message: `SSH connection to ${sshHost}:${sshPort} successful, but backup path is not accessible: ${backupPath}`,
});
}
/**
* Generic SSH test: connect and run a simple echo command.
*/
async function testGenericSsh(config: Record<string, any>, sshHost: string, sshPort: number) {
const sshConfig = extractSshConfig({ ...config, connectionMode: "ssh" });
if (!sshConfig) {
return NextResponse.json(
{ success: false, message: "Invalid SSH configuration" },
{ status: 400 }
);
}

if (!pathResult.writable) {
return NextResponse.json({
success: false,
message: `SSH connection to ${sshHost}:${sshPort} successful, but backup path is read-only: ${backupPath}`,
});
}
const ssh = new SshClient();
try {
await ssh.connect(sshConfig);
const result = await ssh.exec("echo connected");

if (result.code === 0) {
return NextResponse.json({
success: true,
message: `SSH connection to ${sshHost}:${sshPort} successful — backup path ${backupPath} is readable and writable`,
message: `SSH connection to ${sshHost}:${sshPort} successful`,
});
} catch (connectError: unknown) {
sshTransfer.end();
const message =
connectError instanceof Error
? connectError.message
: "SSH connection failed";
}

return NextResponse.json({
success: false,
message: `SSH connected but test command failed: ${result.stderr}`,
});
} catch (connectError: unknown) {
const message =
connectError instanceof Error
? connectError.message
: "SSH connection failed";
log.warn("SSH test failed", { sshHost }, wrapError(connectError));
return NextResponse.json({ success: false, message });
} finally {
ssh.end();
}
}

/**
* MSSQL-specific SSH test: SFTP connect + backup path check.
*/
async function testMssqlSsh(config: MSSQLConfig, sshHost: string, sshPort: number) {
const sshTransfer = new MssqlSshTransfer();

try {
await sshTransfer.connect(config);

const backupPath = config.backupPath || "/var/opt/mssql/backup";
const pathResult = await sshTransfer.testBackupPath(backupPath);

log.warn("SSH test failed", { sshHost }, wrapError(connectError));
sshTransfer.end();

if (!pathResult.readable) {
return NextResponse.json({
success: false,
message,
message: `SSH connection to ${sshHost}:${sshPort} successful, but backup path is not accessible: ${backupPath}`,
});
}
} catch (error: unknown) {
log.error("SSH test route error", {}, wrapError(error));

if (!pathResult.writable) {
return NextResponse.json({
success: false,
message: `SSH connection to ${sshHost}:${sshPort} successful, but backup path is read-only: ${backupPath}`,
});
}

return NextResponse.json({
success: true,
message: `SSH connection to ${sshHost}:${sshPort} successful — backup path ${backupPath} is readable and writable`,
});
} catch (connectError: unknown) {
sshTransfer.end();
const message =
error instanceof Error ? error.message : "Unknown error";
return NextResponse.json(
{ success: false, message },
{ status: 500 }
);
connectError instanceof Error
? connectError.message
: "SSH connection failed";

log.warn("SSH test failed", { sshHost }, wrapError(connectError));

return NextResponse.json({
success: false,
message,
});
}
}
Loading
Loading