diff --git a/app/package.json b/app/package.json
index 7fdc36f..c44e96f 100644
--- a/app/package.json
+++ b/app/package.json
@@ -22,6 +22,7 @@
"dompurify": "^3.2.4",
"marked": "^10.0.0",
"pinia": "^2.1.7",
+ "reconnecting-websocket": "^4.4.0",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
@@ -34,12 +35,12 @@
},
"devDependencies": {
"@types/validator": "^13.11.9",
+ "@vitejs/plugin-vue": "^5.2.2",
"@vue/compiler-sfc": "^3.3.13",
+ "@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
- "@vitejs/plugin-vue": "^5.2.2",
- "@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2",
"vite": "^6.3.1",
"vue-tsc": "^2.2.8"
diff --git a/app/src/components/game/QuestPart.vue b/app/src/components/game/QuestPart.vue
index abffd60..7ef9630 100644
--- a/app/src/components/game/QuestPart.vue
+++ b/app/src/components/game/QuestPart.vue
@@ -22,7 +22,11 @@ const tasks = computed(() => {
{{ sessionStore.quest.title }}
- {{ sessionStore.session?.token }}
+
+ Open Terminal
+
diff --git a/app/src/store/session.ts b/app/src/store/session.ts
index c701142..d19469b 100644
--- a/app/src/store/session.ts
+++ b/app/src/store/session.ts
@@ -50,8 +50,12 @@ const useSession = defineStore('session', {
...session,
graph: new FileGraph(session.graph),
pwd: '/home/commander',
+ containerName: session.containerName || '',
}
term.reset()
+ term.connect(
+ `/terminal/${session.containerName}/ws?token=${session.token}`
+ )
await socket.connect(this.session)
term.focus()
},
diff --git a/app/src/types.ts b/app/src/types.ts
index ec4a6ea..a057d56 100644
--- a/app/src/types.ts
+++ b/app/src/types.ts
@@ -22,4 +22,5 @@ export interface Session {
tasks: ITask[]
responses: IResponse[]
token: string
+ containerName: string
}
diff --git a/app/src/types/css.d.ts b/app/src/types/css.d.ts
new file mode 100644
index 0000000..8be70df
--- /dev/null
+++ b/app/src/types/css.d.ts
@@ -0,0 +1,4 @@
+declare module '*.css' {
+ const content: string
+ export default content
+}
diff --git a/app/src/utils/terminal.ts b/app/src/utils/terminal.ts
index b384844..a585900 100644
--- a/app/src/utils/terminal.ts
+++ b/app/src/utils/terminal.ts
@@ -1,10 +1,12 @@
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
+import ReconnectingWebSocket from 'reconnecting-websocket'
import 'xterm/css/xterm.css'
class SocketTerminal {
private term: Terminal
private fitAddon: FitAddon
+ private socket: ReconnectingWebSocket | null = null
constructor() {
this.term = new Terminal({
@@ -19,6 +21,40 @@ class SocketTerminal {
})
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
+ this.term.onData((data) => {
+ if (this.socket) {
+ this.socket.send(data)
+ }
+ })
+ }
+
+ connect(url: string): void {
+ console.log('Connecting to terminal', url)
+ this.socket = new ReconnectingWebSocket(url)
+ this.socket.onopen = () => {
+ console.log('Connected to terminal')
+ this.term.write('\r\n\x1B[1;32mConnected to terminal\x1B[0m\r\n')
+ }
+
+ this.socket.onclose = () => {
+ console.log('Disconnected from terminal')
+ this.term.write('\r\n\x1B[1;31mDisconnected from terminal\x1B[0m\r\n')
+ }
+
+ this.socket.onerror = (error) => {
+ console.error('WebSocket error:', error)
+ this.term.write(
+ `\r\n\x1B[1;31mWebSocket error: ${'message' in error ? error.message : ''}\x1B[0m\r\n`
+ )
+ }
+ this.socket.onmessage = (event) => {
+ console.log('Received message from terminal')
+ const data =
+ typeof event.data === 'string'
+ ? event.data
+ : new TextDecoder().decode(event.data)
+ this.term.write(data)
+ }
}
resizeScreen(): void {
@@ -38,16 +74,13 @@ class SocketTerminal {
this.term.write(message)
}
- onData(callback: (data: string) => void): void {
- this.term.onData(callback)
- }
-
focus(): void {
this.term.focus()
}
reset(): void {
this.term.reset()
+ this.socket?.close()
}
}
diff --git a/packages/models/session.ts b/packages/models/session.ts
index cae81e8..30f7d4e 100644
--- a/packages/models/session.ts
+++ b/packages/models/session.ts
@@ -28,6 +28,7 @@ export interface ISession {
user: Types.ObjectId
quest: string
containerId: string | null
+ containerName: string | null
status: 'active' | 'finished' | 'inactive'
createdAt: Date
updatedAt: Date
@@ -52,6 +53,7 @@ export const Session = model(
required: true,
},
containerId: String,
+ containerName: String,
status: {
type: String,
enum: ['active', 'finished', 'inactive'],
diff --git a/server/src/models/sessionManager.ts b/server/src/models/sessionManager.ts
index c6bb814..046235d 100644
--- a/server/src/models/sessionManager.ts
+++ b/server/src/models/sessionManager.ts
@@ -61,6 +61,7 @@ export async function createNewSession(
user: userId,
quest: quest.id,
containerId: container.id,
+ containerName,
})
console.log('container', container.id)
diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts
index c5dad08..3c5ecbf 100644
--- a/server/src/routers/sessionRouter.ts
+++ b/server/src/routers/sessionRouter.ts
@@ -38,6 +38,7 @@ async function sessionDetail(session: ISession) {
responses: gameSession.getResponses(),
tasks: gameSession.getTasks(),
graph: gameSession.getGraph(),
+ containerName: session.containerName,
token,
}
}
diff --git a/services/terminal-service/main.go b/services/terminal-service/main.go
index da3b6b7..9d1ee88 100644
--- a/services/terminal-service/main.go
+++ b/services/terminal-service/main.go
@@ -13,6 +13,7 @@ var (
)
func serveWs(w http.ResponseWriter, r *http.Request) {
+ fmt.Println("Serving WS connection")
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade:", err)
diff --git a/traefik/conf/dev.yml b/traefik/conf/dev.yml
index dd0d99f..a73b692 100644
--- a/traefik/conf/dev.yml
+++ b/traefik/conf/dev.yml
@@ -6,8 +6,26 @@ http:
entryPoints:
- web
- backend:
- rule: 'PathPrefix(`/api`, `/socket.io`, `/trpc`, `/api-docs`)'
+ backend-api:
+ rule: 'PathPrefix(`/api`)'
+ service: backend
+ entryPoints:
+ - web
+
+ backend-socket:
+ rule: 'PathPrefix(`/socket.io`)'
+ service: backend
+ entryPoints:
+ - web
+
+ backend-trpc:
+ rule: 'PathPrefix(`/trpc`)'
+ service: backend
+ entryPoints:
+ - web
+
+ backend-api-docs:
+ rule: 'PathPrefix(`/api-docs`)'
service: backend
entryPoints:
- web
diff --git a/yarn.lock b/yarn.lock
index 3c13f5b..ee06019 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8179,6 +8179,11 @@ realize-package-specifier@~3.0.1:
dezalgo "^1.0.1"
npm-package-arg "^4.1.1"
+reconnecting-websocket@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+ integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
+
regenerate-unicode-properties@^10.1.0:
version "10.1.1"
resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz"