diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..8d443e8a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Read(/C:\\Users\\29037\\WebstormProjects\\Termix\\docker/**)", + "Bash(git fetch:*)", + "Bash(git pull:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(grep:*)", + "Bash(git push:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/.env b/.env index c1e19f61..99ffac00 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VERSION=1.5.0 \ No newline at end of file +VERSION=1.6.0 +VITE_API_HOST=localhost diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 4a7cecd9..b2bf6f80 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -73,7 +73,7 @@ jobs: elif [ "${{ github.ref }}" == "refs/heads/development" ]; then IMAGE_TAG="development-latest" else - IMAGE_TAG="${{ github.ref_name }}-development-latest" + IMAGE_TAG="${{ github.ref_name }}" fi echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index d0adddb5..d643c797 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? /db/ +/release/ diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 00000000..54611470 --- /dev/null +++ b/README-CN.md @@ -0,0 +1,100 @@ +# Repo Stats + +
+ English |
+
中文
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ +# License +根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。 + diff --git a/README.md b/README.md index 72d47b80..e14e5f0c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ # Repo Stats +
+ English |
+
中文
+
sudo apt install
sshpass (Debian/Ubuntu) or the equivalent for your OS.
{t('hosts.otherInstallMethods')}
+ • {t('hosts.centosRhelFedora')} sudo yum install
sshpass or sudo dnf install
sshpass
- • macOS: brew
+ • {t('hosts.macos')} brew
install hudochenkov/sshpass/sshpass
- • Windows: Use WSL or consider SSH key authentication
+ • {t('hosts.windows')}
- SSH Server Configuration Required
- For reverse SSH tunnels, the endpoint SSH server must allow:
+ {t('hosts.sshServerConfigRequired')}
+ {t('hosts.sshServerConfigDesc')}
• GatewayPorts
- yes (bind remote ports)
+ yes {t('hosts.gatewayPortsYes')}
• AllowTcpForwarding
- yes (port forwarding)
+ yes {t('hosts.allowTcpForwardingYes')}
• PermitRootLogin
- yes (if using root)
+ yes {t('hosts.permitRootLoginYes')}
- Edit /etc/ssh/sshd_config and
- restart SSH: sudo
- systemctl restart sshd
+ {t('hosts.editSshConfig')}
@@ -783,7 +784,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name="tunnelConnections"
render={({field}) => (
- Tunnel Connections
+ {t('hosts.tunnelConnections')}
{field.value.map((connection, index) => (
@@ -791,7 +792,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
className="p-4 border rounded-lg bg-muted/50">
- Connection {index + 1}
+ {t('hosts.connection')} {index + 1}
@@ -810,10 +811,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name={`tunnelConnections.${index}.sourcePort`}
render={({field: sourcePortField}) => (
- Source Port
- (Source refers to the Current
- Connection Details in the
- General tab)
+ {t('hosts.sourcePort')}
+ {t('hosts.sourcePortDesc')}
@@ -826,8 +825,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name={`tunnelConnections.${index}.endpointPort`}
render={({field: endpointPortField}) => (
- Endpoint Port
- (Remote)
+ {t('hosts.endpointPort')}
@@ -841,14 +839,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
render={({field: endpointHostField}) => (
- Endpoint SSH
- Configuration
+ {t('hosts.endpointSshConfig')}
{
sshConfigInputRefs.current[index] = el;
}}
- placeholder="endpoint ssh configuration"
+ placeholder={t('placeholders.sshConfig')}
className="min-h-[40px]"
autoComplete="off"
value={endpointHostField.value}
@@ -895,12 +892,10 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
- This tunnel will forward traffic from
- port {form.watch(`tunnelConnections.${index}.sourcePort`) || '22'} on
- the source machine (current connection details
- in general tab) to
- port {form.watch(`tunnelConnections.${index}.endpointPort`) || '224'} on
- the endpoint machine.
+ {t('hosts.tunnelForwardDescription', {
+ sourcePort: form.watch(`tunnelConnections.${index}.sourcePort`) || '22',
+ endpointPort: form.watch(`tunnelConnections.${index}.endpointPort`) || '224'
+ })}
@@ -909,14 +904,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name={`tunnelConnections.${index}.maxRetries`}
render={({field: maxRetriesField}) => (
- Max Retries
+ {t('hosts.maxRetries')}
+ placeholder={t('placeholders.maxRetries')} {...maxRetriesField} />
- Maximum number of retry attempts
- for tunnel connection.
+ {t('hosts.maxRetriesDescription')}
)}
@@ -926,15 +920,13 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name={`tunnelConnections.${index}.retryInterval`}
render={({field: retryIntervalField}) => (
- Retry Interval
- (seconds)
+ {t('hosts.retryInterval')}
+ placeholder={t('placeholders.retryInterval')} {...retryIntervalField} />
- Time to wait between retry
- attempts.
+ {t('hosts.retryIntervalDescription')}
)}
@@ -944,8 +936,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name={`tunnelConnections.${index}.autoStart`}
render={({field}) => (
- Auto Start on Container
- Launch
+ {t('hosts.autoStartContainer')}
- Automatically start this tunnel
- when the container launches.
+ {t('hosts.autoStartDesc')}
)}
@@ -976,7 +966,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
}]);
}}
>
- Add Tunnel Connection
+ {t('hosts.addConnection')}
@@ -994,7 +984,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name="enableFileManager"
render={({field}) => (
- Enable File Manager
+ {t('hosts.enableFileManager')}
- Enable/disable host visibility in File Manager tab.
+ {t('hosts.enableFileManagerDesc')}
)}
@@ -1015,12 +1005,11 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
name="defaultPath"
render={({field}) => (
- Default Path
+ {t('hosts.defaultPath')}
-
+
- Set default directory shown when connected via
- File Manager
+ {t('hosts.defaultPathDesc')}
)}
/>
@@ -1039,7 +1028,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
transform: 'translateY(8px)'
}}
>
- {editingHost ? "Update Host" : "Add Host"}
+ {editingHost ? t('hosts.updateHost') : t('hosts.addHost')}
diff --git a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerHostViewer.tsx
similarity index 89%
rename from src/ui/Apps/Host Manager/HostManagerHostViewer.tsx
rename to src/ui/Desktop/Apps/Host Manager/HostManagerHostViewer.tsx
index 476eb895..e50cf1cb 100644
--- a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx
+++ b/src/ui/Desktop/Apps/Host Manager/HostManagerHostViewer.tsx
@@ -1,12 +1,14 @@
import React, {useState, useEffect, useMemo} from "react";
-import {Card, CardContent} from "@/components/ui/card";
-import {Button} from "@/components/ui/button";
-import {Badge} from "@/components/ui/badge";
-import {ScrollArea} from "@/components/ui/scroll-area";
-import {Input} from "@/components/ui/input";
-import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
-import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
+import {Card, CardContent} from "@/components/ui/card.tsx";
+import {Button} from "@/components/ui/button.tsx";
+import {Badge} from "@/components/ui/badge.tsx";
+import {ScrollArea} from "@/components/ui/scroll-area.tsx";
+import {Input} from "@/components/ui/input.tsx";
+import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion.tsx";
+import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip.tsx";
import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
+import {toast} from "sonner";
+import {useTranslation} from "react-i18next";
import {
Edit,
Trash2,
@@ -47,6 +49,7 @@ interface SSHManagerHostViewerProps {
}
export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
+ const {t} = useTranslation();
const [hosts, setHosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -64,20 +67,21 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
setHosts(data);
setError(null);
} catch (err) {
- setError('Failed to load hosts');
+ setError(t('hosts.failedToLoadHosts'));
} finally {
setLoading(false);
}
};
const handleDelete = async (hostId: number, hostName: string) => {
- if (window.confirm(`Are you sure you want to delete "${hostName}"?`)) {
+ if (window.confirm(t('hosts.confirmDelete', { name: hostName }))) {
try {
await deleteSSHHost(hostId);
+ toast.success(t('hosts.hostDeletedSuccessfully', { name: hostName }));
await fetchHosts();
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
} catch (err) {
- alert('Failed to delete host');
+ toast.error(t('hosts.failedToDeleteHost'));
}
}
};
@@ -98,32 +102,35 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
const data = JSON.parse(text);
if (!Array.isArray(data.hosts) && !Array.isArray(data)) {
- throw new Error('JSON must contain a "hosts" array or be an array of hosts');
+ throw new Error(t('hosts.jsonMustContainHosts'));
}
const hostsArray = Array.isArray(data.hosts) ? data.hosts : data;
if (hostsArray.length === 0) {
- throw new Error('No hosts found in JSON file');
+ throw new Error(t('hosts.noHostsInJson'));
}
if (hostsArray.length > 100) {
- throw new Error('Maximum 100 hosts allowed per import');
+ throw new Error(t('hosts.maxHostsAllowed'));
}
const result = await bulkImportSSHHosts(hostsArray);
if (result.success > 0) {
- alert(`Import completed: ${result.success} successful, ${result.failed} failed${result.errors.length > 0 ? '\n\nErrors:\n' + result.errors.join('\n') : ''}`);
+ toast.success(t('hosts.importCompleted', { success: result.success, failed: result.failed }));
+ if (result.errors.length > 0) {
+ toast.error(`Import errors: ${result.errors.join(', ')}`);
+ }
await fetchHosts();
window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
} else {
- alert(`Import failed: ${result.errors.join('\n')}`);
+ toast.error(t('hosts.importFailed') + `: ${result.errors.join(', ')}`);
}
} catch (err) {
- const errorMessage = err instanceof Error ? err.message : 'Failed to import JSON file';
- alert(`Import error: ${errorMessage}`);
+ const errorMessage = err instanceof Error ? err.message : t('hosts.failedToImportJson');
+ toast.error(t('hosts.importError') + `: ${errorMessage}`);
} finally {
setImporting(false);
event.target.value = '';
@@ -163,7 +170,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
const grouped: { [key: string]: SSHHost[] } = {};
filteredAndSortedHosts.forEach(host => {
- const folder = host.folder || 'Uncategorized';
+ const folder = host.folder || t('hosts.uncategorized');
if (!grouped[folder]) {
grouped[folder] = [];
}
@@ -171,8 +178,8 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
});
const sortedFolders = Object.keys(grouped).sort((a, b) => {
- if (a === 'Uncategorized') return -1;
- if (b === 'Uncategorized') return 1;
+ if (a === t('hosts.uncategorized')) return -1;
+ if (b === t('hosts.uncategorized')) return 1;
return a.localeCompare(b);
});
@@ -189,7 +196,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
- Loading hosts...
+ {t('hosts.loadingHosts')}
);
@@ -201,7 +208,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
{error}
@@ -213,9 +220,9 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
- No SSH Hosts
+ {t('hosts.noHosts')}
- You haven't added any SSH hosts yet. Click "Add Host" to get started.
+ {t('hosts.noHostsMessage')}
@@ -226,9 +233,9 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
- SSH Hosts
+ {t('hosts.sshHosts')}
- {filteredAndSortedHosts.length} hosts
+ {t('hosts.hostsCount', { count: filteredAndSortedHosts.length })}
@@ -242,15 +249,15 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
onClick={() => document.getElementById('json-import-input')?.click()}
disabled={importing}
>
- {importing ? 'Importing...' : 'Import JSON'}
+ {importing ? t('hosts.importing') : t('hosts.importJson')}
- Import SSH Hosts from JSON
+ {t('hosts.importJsonTitle')}
- Upload a JSON file to bulk import multiple SSH hosts (max 100).
+ {t('hosts.importJsonDesc')}
@@ -318,7 +325,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
URL.revokeObjectURL(url);
}}
>
- Download Sample
+ {t('hosts.downloadSample')}
@@ -350,7 +357,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
setSearchQuery(e.target.value)}
className="pl-10"
@@ -446,13 +453,13 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
{host.enableTerminal && (
- Terminal
+ {t('hosts.terminalBadge')}
)}
{host.enableTunnel && (
- Tunnel
+ {t('hosts.tunnelBadge')}
{host.tunnelConnections && host.tunnelConnections.length > 0 && (
({host.tunnelConnections.length})
@@ -462,7 +469,7 @@ export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
{host.enableFileManager && (
- File Manager
+ {t('hosts.fileManagerBadge')}
)}
diff --git a/src/ui/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx
similarity index 98%
rename from src/ui/Apps/Server/Server.tsx
rename to src/ui/Desktop/Apps/Server/Server.tsx
index 413c0a78..b58b3410 100644
--- a/src/ui/Apps/Server/Server.tsx
+++ b/src/ui/Desktop/Apps/Server/Server.tsx
@@ -1,13 +1,13 @@
import React from "react";
-import {useSidebar} from "@/components/ui/sidebar";
+import {useSidebar} from "@/components/ui/sidebar.tsx";
import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
import {Separator} from "@/components/ui/separator.tsx";
import {Button} from "@/components/ui/button.tsx";
-import {Progress} from "@/components/ui/progress"
+import {Progress} from "@/components/ui/progress.tsx"
import {Cpu, HardDrive, MemoryStick} from "lucide-react";
-import {Tunnel} from "@/ui/Apps/Tunnel/Tunnel.tsx";
+import {Tunnel} from "@/ui/Desktop/Apps/Tunnel/Tunnel.tsx";
import {getServerStatusById, getServerMetricsById, type ServerMetrics} from "@/ui/main-axios.ts";
-import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
+import {useTabs} from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
import {useTranslation} from 'react-i18next';
interface ServerProps {
diff --git a/src/ui/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx
similarity index 97%
rename from src/ui/Apps/Terminal/Terminal.tsx
rename to src/ui/Desktop/Apps/Terminal/Terminal.tsx
index 1ae72427..2b57c94f 100644
--- a/src/ui/Apps/Terminal/Terminal.tsx
+++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx
@@ -279,9 +279,13 @@ export const Terminal = forwardRef(function SSHTerminal(
const isDev = process.env.NODE_ENV === 'development' &&
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
+
+ const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true;
const wsUrl = isDev
? 'ws://localhost:8082'
+ : isElectron
+ ? 'ws://127.0.0.1:8082'
: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
const ws = new WebSocket(wsUrl);
@@ -357,7 +361,7 @@ style.innerHTML = `
/* Load NerdFonts locally */
@font-face {
font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
+ src: url('./fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
@@ -365,7 +369,7 @@ style.innerHTML = `
@font-face {
font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
+ src: url('./fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: swap;
@@ -373,7 +377,7 @@ style.innerHTML = `
@font-face {
font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
+ src: url('./fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
font-weight: normal;
font-style: italic;
font-display: swap;
diff --git a/src/ui/Apps/Tunnel/Tunnel.tsx b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx
similarity index 99%
rename from src/ui/Apps/Tunnel/Tunnel.tsx
rename to src/ui/Desktop/Apps/Tunnel/Tunnel.tsx
index e900f0eb..6c45bca8 100644
--- a/src/ui/Apps/Tunnel/Tunnel.tsx
+++ b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx
@@ -1,5 +1,5 @@
import React, {useState, useEffect, useCallback} from "react";
-import {TunnelViewer} from "@/ui/Apps/Tunnel/TunnelViewer.tsx";
+import {TunnelViewer} from "@/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx";
import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts";
interface TunnelConnection {
diff --git a/src/ui/Apps/Tunnel/TunnelObject.tsx b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx
similarity index 98%
rename from src/ui/Apps/Tunnel/TunnelObject.tsx
rename to src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx
index 13c5f4d4..52105a53 100644
--- a/src/ui/Apps/Tunnel/TunnelObject.tsx
+++ b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx
@@ -236,7 +236,7 @@ export function TunnelObject({
className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs"
>
- Connect
+ {t('tunnels.connect')}
)}
@@ -299,7 +299,7 @@ export function TunnelObject({
) : (
- No tunnel connections configured
+ {t('tunnels.noTunnelConnections')}
)}
No tunnel connections configured
+{t('tunnels.noTunnelConnections')}
Enter the 6-digit code from the docker container logs for - user: {userInfo.username}
+{t('common.enterSixDigitCode')} {userInfo.username}
Enter your new password for - user: {userInfo.username}
+{t('common.enterNewPassword')} {userInfo.username}