Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions images/chromium-headful/client/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
</div>
</main>
<neko-side v-if="!videoOnly && side" />
<neko-connect v-if="!connected" />
<neko-connect v-if="!connected && !wasConnected" />
<neko-disconnected v-if="!connected && wasConnected" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One edge case with wasConnected: if there’s an explicit logout flow (or any future “start a new session” flow), this flag never resets so you can get stuck on the disconnected overlay forever. Might be worth either (a) renaming it to something like hasEverConnected to make the semantics explicit, and/or (b) resetting it when initiating a new login attempt (or when the store indicates a new session).

<neko-about v-if="about" />
<notifications
v-if="!videoOnly"
Expand Down Expand Up @@ -175,6 +176,7 @@
import { Vue, Component, Ref, Watch } from 'vue-property-decorator'

import Connect from '~/components/connect.vue'
import Disconnected from '~/components/disconnected.vue'
import Video from '~/components/video.vue'
import Menu from '~/components/menu.vue'
import Side from '~/components/side.vue'
Expand All @@ -189,6 +191,7 @@
name: 'neko',
components: {
'neko-connect': Connect,
'neko-disconnected': Disconnected,
'neko-video': Video,
// 'neko-menu': Menu,
//'neko-side': Side,
Expand All @@ -204,6 +207,7 @@
@Ref('video') video!: Video

shakeKbd = false
wasConnected = false

get volume() {
const numberParam = parseFloat(new URL(location.href).searchParams.get('volume') || '1.0')
Expand Down Expand Up @@ -272,6 +276,7 @@
@Watch('connected', { immediate: true })
onConnected(value: boolean) {
if (value) {
this.wasConnected = true
this.applyQueryResolution()
try {
if (window.parent !== window) {
Expand Down Expand Up @@ -364,6 +369,11 @@
}
}


@Watch('connected', { immediate: true })
onConnectedChange(connected: boolean) {
if (connected) {
this.wasConnected = true
}
}
}
</script>
49 changes: 0 additions & 49 deletions images/chromium-headful/client/src/components/connect.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
<template>
<div class="connect">
<div class="window">
<form class="message" v-if="!connecting" @submit.stop.prevent="connect">
<span v-if="!autoPassword">{{ $t('connect.login_title') }}</span>
<span v-else>{{ $t('connect.invitation_title') }}</span>
<input type="text" :placeholder="$t('connect.displayname')" v-model="displayname" />
<input type="password" :placeholder="$t('connect.password')" v-model="password" v-if="!autoPassword" />
<button type="submit" @click.stop.prevent="login">
{{ $t('connect.connect') }}
</button>
</form>
<div class="loader" v-if="connecting">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the login form is gone, v-if="connecting" can leave an empty overlay if connecting briefly flips false/undefined during init. Making the loader unconditional avoids a blank screen.

Suggested change
<div class="loader" v-if="connecting">
<div class="loader">

<img src="../assets/images/logo.svg" alt="loading" aria-hidden="true" class="kernel-logo" />
</div>
Expand Down Expand Up @@ -54,46 +45,6 @@
}
}
.message {
display: flex;
flex-direction: column;
span {
display: block;
text-align: center;
text-transform: uppercase;
line-height: 30px;
}
input {
border: none;
padding: 6px 8px;
line-height: 20px;
border-radius: 5px;
margin: 5px 0;
background: $background-tertiary;
color: $text-normal;
&::selection {
background: $text-link;
}
}
button {
cursor: pointer;
border-radius: 5px;
padding: 4px;
background: $style-primary;
color: $text-normal;
text-align: center;
text-transform: uppercase;
font-weight: bold;
line-height: 30px;
margin: 5px 0;
border: none;
}
}
.loader {
width: 90px;
height: 90px;
Expand Down
38 changes: 38 additions & 0 deletions images/chromium-headful/client/src/components/disconnected.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<template>
<div class="disconnected-overlay">
<div class="message-container">
<span class="message">{{ $t('disconnected.message') }}</span>
</div>
</div>
</template>

<style lang="scss" scoped>
.disconnected-overlay {
width: 100vw;
height: 100vh;
background: #000;
display: flex;
justify-content: center;
align-items: center;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disconnected overlay missing position fixed for proper layering

High Severity

The .disconnected-overlay CSS uses width: 100vw; height: 100vh but lacks position: fixed (or position: absolute with positioning offsets). Since the parent #neko container uses display: flex; flex-direction: row, this overlay becomes a flex item laid out alongside main.neko-main rather than overlaying the content. Other overlay components like neko-connect and neko-about correctly use position: fixed/absolute with top/left/right/bottom: 0 to achieve proper overlay behavior.

Fix in Cursor Fix in Web

.message-container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
.message {
font-size: 18px;
color: $text-muted;
text-align: center;
}
}
}
</style>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
@Component({ name: 'neko-disconnected' })
export default class extends Vue {}
</script>
4 changes: 4 additions & 0 deletions images/chromium-headful/client/src/locale/en-us.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,7 @@ export const files = {
uploads: 'Uploads',
upload_here: 'Click or drag files here to upload',
}

export const disconnected = {
message: 'Browser has been shut down and is no longer available',
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing translation for disconnected message in non-English locales

Medium Severity

The new disconnected translation object was only added to en-us.ts, but the application supports 12 other locales (de-de, es-sp, sk-sk, sv-se, nb-no, fr-fr, ko-kr, fi-fi, ru-ru, zh-cn, zh-tw, ja-jp). When users with non-English locales view the disconnected screen, $t('disconnected.message') will fail to find the translation, causing either the raw key string to display or unexpected fallback behavior instead of a proper localized message.

Fix in Cursor Fix in Web

7 changes: 5 additions & 2 deletions images/chromium-headful/client/src/neko/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
)
this.emit('debug', `connecting to ${this._ws.url}`)
this._ws.onmessage = this.onMessage.bind(this)
this._ws.onerror = () => this.onError.bind(this)
this._ws.onclose = () => this.onDisconnected.bind(this, new Error('websocket closed'))
this._ws.onerror = this.onError.bind(this)
this._ws.onclose = (event) => {
this.emit('debug', `websocket closed: code=${event.code}, reason=${event.reason}`)
this.onDisconnected(new Error('websocket closed'))
}
Comment on lines +73 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix on the event handler binding.

Right now we log event.code/event.reason but the user-facing Error('websocket closed') drops that context (it ends up in the disconnected notification text). Might be worth carrying the details into the Error message.

Suggested change
this._ws.onclose = (event) => {
this.emit('debug', `websocket closed: code=${event.code}, reason=${event.reason}`)
this.onDisconnected(new Error('websocket closed'))
}
this._ws.onclose = (event) => {
const message = `websocket closed: code=${event.code}, reason=${event.reason || 'unknown'}`
this.emit('debug', message)
this.onDisconnected(new Error(message))
}

this._timeout = window.setTimeout(this.onTimeout.bind(this), 15000)
} catch (err: any) {
this.onDisconnected(err)
Expand Down
15 changes: 8 additions & 7 deletions images/chromium-headful/client/src/neko/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,14 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
// Internal Events
/////////////////////////////
protected [EVENT.RECONNECTING]() {
this.$vue.$notify({
group: 'neko',
type: 'warning',
title: this.$vue.$t('connection.reconnecting') as string,
duration: 5000,
speed: 1000,
})
// KERNEL: Differentiate between temporary network issues and permanent disconnection
// If WebSocket is still open, this is likely a temporary ICE disconnection (network glitch)
// Allow WebRTC to attempt recovery automatically
// If WebSocket is closed, the browser process is likely gone - show disconnected overlay
if (!this.socketOpen) {
this.cleanup()
}
// else: WebSocket still open, let WebRTC recover naturally
}

protected [EVENT.CONNECTING]() {
Expand Down
Loading