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
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,33 @@ pulls notes from — it's the minimum, the GitHub release page is the maximum.

## [Unreleased]

## [2.7.4] - 2026-05-27

> [Full notes](https://github.com/Kitsune-Den/KitsuneCommand/releases/tag/v2.7.4)
> · Patch release — the KC admin password no longer rotates on new
> worlds. Persistent data (DB, FIRST_RUN / RESET password files) now
> lives in a world-agnostic location.

### Fixed

- **Admin password no longer rotates on world regen.** The KC data dir
was anchored to `GameIO.GetSaveGameDir()`, which returns the
*current world's* save folder. Every new world (or any 7DTD boot
that landed on a different save dir) produced an empty KC database
and re-ran `AuthService.EnsureAdminExists`, silently rotating the
admin password and writing a fresh `FIRST_RUN_PASSWORD.txt`.
Observed on a live server as four "FIRST RUN" blocks in two days,
each invalidating the operator's stored panel creds with no obvious
cause. Fix: anchor the data dir to the 7DTD user-data root (parent
of `Saves/`) so the DB, `appsettings.json` override,
`FIRST_RUN_PASSWORD.txt`, and `RESET_PASSWORD.txt` survive world
regen, save deletion, and PackRelay mod re-installs. Includes a
best-effort one-time migration that copies any existing per-world
data forward on first boot with the new code. New
`ConfigManager.ResolveWorldAgnosticDataDir()` is the single source
of truth — `AuthService` and `WebServerHost` both call it instead
of duplicating the path walk.

## [2.7.3] - 2026-05-19

> [Full notes](https://github.com/Kitsune-Den/KitsuneCommand/releases/tag/v2.7.3)
Expand Down Expand Up @@ -489,7 +516,8 @@ The 2.0 cut, not a continuation of v1.x.

---

[Unreleased]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.3...HEAD
[Unreleased]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.4...HEAD
[2.7.4]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.3...v2.7.4
[2.7.3]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.2...v2.7.3
[2.7.2]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.1...v2.7.2
[2.7.1]: https://github.com/Kitsune-Den/KitsuneCommand/compare/v2.7.0...v2.7.1
Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "kitsunecommand-frontend",
"private": true,
"version": "2.7.3",
"version": "2.7.4",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/api/joinAttempts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import apiClient from './client'

/**
* One event in a client's connection lifecycle on the server side, captured
* by KitsuneCommand's AuthWrapperServerDiagnostics Harmony patches. One
* "click Direct Connect" typically produces 10-30 of these as LiteNetLib
* retries and the auth-state state machine transitions — bursts that the
* panel renders as one logical "join attempt" by grouping on peerIp:peerPort.
*
* Matches the JSON shape returned by the C# `JoinAttemptEvent` (see
* `KitsuneCommand/Diagnostics/JoinAttemptEvent.cs`).
*/
export interface JoinAttemptEvent {
/** UTC ISO-8601 timestamp the event was recorded. */
timestamp: string

/** One of: ConnReq, Recv, Conn, Disc, Update. */
eventType: string

/** Source IP. Null for Update events. */
peerIp: string | null

/** Source port. Null for Update events. */
peerPort: number | null

/**
* For ConnReq: Accept / Reject / RejectForce / None.
* For Disc: LiteNetLib DisconnectReason name (PeerNotFound, Timeout,
* DisconnectPeerCalled, etc.) — the field operators most care about.
* Null otherwise.
*/
result: string | null

/**
* ConnReq payload size in bytes. 2 = bare LiteNetLib version handshake,
* 0 = wrapper already consumed it (Accept happened). Null otherwise.
*/
dataBytes: number | null

/** Channel byte for Recv events. */
channel: number | null

/** ReliableOrdered / Unreliable / ... for Recv events. */
deliveryMethod: string | null

/** Size of disconnect packet's optional payload (Disc events only). */
extraDataBytes: number | null

/** authStates dict size at the moment of this event. */
authStateCount: number | null
}

export interface JoinAttemptListResponse {
events: JoinAttemptEvent[]
/** Process-lifetime monotonic counter — useful for "Hey, the ring is filling fast". */
totalRecorded: number
/** Whether [KC-NetDiag] verbose console logging is currently on. */
verboseLogging: boolean
/** Ring buffer capacity (events older than this get overwritten). */
capacity: number
}

/**
* Fetch up to `limit` most-recent events, optionally only those at or after
* `since` (ISO-8601). Returns newest-first.
*/
export async function getJoinAttempts(
limit: number = 100,
since: string | null = null
): Promise<JoinAttemptListResponse> {
const params: Record<string, string | number> = { limit }
if (since) params.since = since
const res = await apiClient.get('/api/join-attempts', { params })
return res.data.data
}

/** Empty the ring buffer. Doesn't reset the monotonic totalRecorded counter. */
export async function clearJoinAttempts(): Promise<string> {
const res = await apiClient.post('/api/join-attempts/clear')
return res.data.message
}

/**
* Turn the [KC-NetDiag] verbose console logging on or off at runtime.
* Ring-buffer recording is unaffected (it's always on).
*/
export async function setVerboseLogging(enabled: boolean): Promise<{ enabled: boolean }> {
const res = await apiClient.post('/api/join-attempts/verbose', { enabled })
return res.data.data
}
1 change: 1 addition & 0 deletions frontend/src/components/layout/AppLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const navItems = computed(() => [
{ label: t('nav.serverControl'), icon: 'pi pi-server', route: '/server' },
{ label: t('nav.serverUpdate'), icon: 'pi pi-sync', route: '/server-update' },
{ label: t('nav.console'), icon: 'pi pi-code', route: '/console' },
{ label: t('nav.joinAttempts'), icon: 'pi pi-sign-in', route: '/join-attempts' },
{ label: t('nav.configEditor'), icon: 'pi pi-file-edit', route: '/config' },
{ label: t('nav.mods'), icon: 'pi pi-box', route: '/mods' },
{ label: t('nav.packRelay'), icon: 'pi pi-cloud-upload', route: '/packrelay' },
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const de = {
dashboard: 'Übersicht',
players: 'Spieler',
console: 'Konsole',
joinAttempts: 'Verbindungsversuche',
map: 'Karte',
chat: 'Chat',
teleport: 'Teleport',
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const en = {
dashboard: 'Dashboard',
players: 'Players',
console: 'Console',
joinAttempts: 'Join Attempts',
map: 'Map',
chat: 'Chat',
teleport: 'Teleport',
Expand Down Expand Up @@ -241,6 +242,28 @@ const en = {
clearLog: 'Clear',
},

joinAttempts: {
title: 'Join Attempts',
subtitle: 'Live view of LiteNetLib-layer connection events. Powered by KitsuneCommand\'s in-process ring buffer. Restart clears the ring.',
totalRecorded: 'Events (lifetime)',
bufferUsage: 'Buffer usage',
verbose: 'Verbose console logging',
verboseOn: 'Now logging each event to nssm-stdout.log as [KC-NetDiag] lines.',
verboseOff: 'Console logging disabled; ring buffer recording continues.',
autoRefresh: 'Auto-refresh',
clear: 'Clear ring',
cleared: 'Join-attempt ring cleared.',
failedToClear: 'Failed to clear ring.',
failedToLoad: 'Failed to load join attempts',
failedToToggle: 'Failed to toggle verbose logging.',
peer: 'Peer',
time: 'When',
events: 'Events',
outcome: 'Outcome',
steps: 'Steps',
empty: 'No join attempts in the ring buffer. Try clicking Direct Connect on a 7DTD client to populate it.',
},

map: {
title: 'Map',
loadingMap: 'Loading map...',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const es = {
dashboard: 'Panel principal',
players: 'Jugadores',
console: 'Consola',
joinAttempts: 'Intentos de conexión',
map: 'Mapa',
chat: 'Chat',
teleport: 'Teletransporte',
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const fr = {
dashboard: 'Tableau de bord',
players: 'Joueurs',
console: 'Console',
joinAttempts: 'Tentatives de connexion',
map: 'Carte',
chat: 'Chat',
teleport: 'Téléportation',
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ja: Messages = {
dashboard: 'ダッシュボード',
players: 'プレイヤー',
console: 'コンソール',
joinAttempts: 'Join Attempts',
map: 'マップ',
chat: 'チャット',
teleport: 'テレポート',
Expand Down Expand Up @@ -245,6 +246,29 @@ const ja: Messages = {
clearLog: 'クリア',
},

// English placeholders pending translation.
joinAttempts: {
title: 'Join Attempts',
subtitle: 'Live view of LiteNetLib-layer connection events. Powered by KitsuneCommand\'s in-process ring buffer. Restart clears the ring.',
totalRecorded: 'Events (lifetime)',
bufferUsage: 'Buffer usage',
verbose: 'Verbose console logging',
verboseOn: 'Now logging each event to nssm-stdout.log as [KC-NetDiag] lines.',
verboseOff: 'Console logging disabled; ring buffer recording continues.',
autoRefresh: 'Auto-refresh',
clear: 'Clear ring',
cleared: 'Join-attempt ring cleared.',
failedToClear: 'Failed to clear ring.',
failedToLoad: 'Failed to load join attempts',
failedToToggle: 'Failed to toggle verbose logging.',
peer: 'Peer',
time: 'When',
events: 'Events',
outcome: 'Outcome',
steps: 'Steps',
empty: 'No join attempts in the ring buffer. Try clicking Direct Connect on a 7DTD client to populate it.',
},

map: {
title: 'マップ',
loadingMap: 'マップを読み込み中...',
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const ko: Messages = {
dashboard: '대시보드',
players: '플레이어',
console: '콘솔',
joinAttempts: 'Join Attempts',
map: '지도',
chat: '채팅',
teleport: '텔레포트',
Expand Down Expand Up @@ -245,6 +246,29 @@ const ko: Messages = {
clearLog: '지우기',
},

// English placeholders pending translation.
joinAttempts: {
title: 'Join Attempts',
subtitle: 'Live view of LiteNetLib-layer connection events. Powered by KitsuneCommand\'s in-process ring buffer. Restart clears the ring.',
totalRecorded: 'Events (lifetime)',
bufferUsage: 'Buffer usage',
verbose: 'Verbose console logging',
verboseOn: 'Now logging each event to nssm-stdout.log as [KC-NetDiag] lines.',
verboseOff: 'Console logging disabled; ring buffer recording continues.',
autoRefresh: 'Auto-refresh',
clear: 'Clear ring',
cleared: 'Join-attempt ring cleared.',
failedToClear: 'Failed to clear ring.',
failedToLoad: 'Failed to load join attempts',
failedToToggle: 'Failed to toggle verbose logging.',
peer: 'Peer',
time: 'When',
events: 'Events',
outcome: 'Outcome',
steps: 'Steps',
empty: 'No join attempts in the ring buffer. Try clicking Direct Connect on a 7DTD client to populate it.',
},

map: {
title: '지도',
loadingMap: '지도 로딩 중...',
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const zhCN: Messages = {
dashboard: '仪表盘',
players: '玩家',
console: '控制台',
joinAttempts: 'Join Attempts',
map: '地图',
chat: '聊天',
teleport: '传送',
Expand Down Expand Up @@ -245,6 +246,29 @@ const zhCN: Messages = {
clearLog: '清除',
},

// English placeholders pending translation.
joinAttempts: {
title: 'Join Attempts',
subtitle: 'Live view of LiteNetLib-layer connection events. Powered by KitsuneCommand\'s in-process ring buffer. Restart clears the ring.',
totalRecorded: 'Events (lifetime)',
bufferUsage: 'Buffer usage',
verbose: 'Verbose console logging',
verboseOn: 'Now logging each event to nssm-stdout.log as [KC-NetDiag] lines.',
verboseOff: 'Console logging disabled; ring buffer recording continues.',
autoRefresh: 'Auto-refresh',
clear: 'Clear ring',
cleared: 'Join-attempt ring cleared.',
failedToClear: 'Failed to clear ring.',
failedToLoad: 'Failed to load join attempts',
failedToToggle: 'Failed to toggle verbose logging.',
peer: 'Peer',
time: 'When',
events: 'Events',
outcome: 'Outcome',
steps: 'Steps',
empty: 'No join attempts in the ring buffer. Try clicking Direct Connect on a 7DTD client to populate it.',
},

map: {
title: '地图',
loadingMap: '正在加载地图...',
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const zhTW: Messages = {
dashboard: '儀表板',
players: '玩家',
console: '主控台',
joinAttempts: 'Join Attempts',
map: '地圖',
chat: '聊天',
teleport: '傳送',
Expand Down Expand Up @@ -245,6 +246,29 @@ const zhTW: Messages = {
clearLog: '清除',
},

// English placeholders pending translation.
joinAttempts: {
title: 'Join Attempts',
subtitle: 'Live view of LiteNetLib-layer connection events. Powered by KitsuneCommand\'s in-process ring buffer. Restart clears the ring.',
totalRecorded: 'Events (lifetime)',
bufferUsage: 'Buffer usage',
verbose: 'Verbose console logging',
verboseOn: 'Now logging each event to nssm-stdout.log as [KC-NetDiag] lines.',
verboseOff: 'Console logging disabled; ring buffer recording continues.',
autoRefresh: 'Auto-refresh',
clear: 'Clear ring',
cleared: 'Join-attempt ring cleared.',
failedToClear: 'Failed to clear ring.',
failedToLoad: 'Failed to load join attempts',
failedToToggle: 'Failed to toggle verbose logging.',
peer: 'Peer',
time: 'When',
events: 'Events',
outcome: 'Outcome',
steps: 'Steps',
empty: 'No join attempts in the ring buffer. Try clicking Direct Connect on a 7DTD client to populate it.',
},

map: {
title: '地圖',
loadingMap: '正在載入地圖...',
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ const router = createRouter({
name: 'Console',
component: () => import('@/views/ConsoleView.vue'),
},
{
// Diagnostic surface for LiteNetLib-layer connection events,
// powered by AuthWrapperServerDiagnostics + JoinAttemptRing.
// Lives alongside /console because it's the same family —
// operator-facing live diagnostics — just structured (events
// table) instead of free-form (log lines).
path: 'join-attempts',
name: 'JoinAttempts',
component: () => import('@/views/JoinAttemptsView.vue'),
},
{
path: 'server',
name: 'ServerControl',
Expand Down
Loading
Loading