Stable state before Final Polish optimizations

This commit is contained in:
Timo
2026-04-22 08:56:52 +02:00
parent d558e9d9c4
commit 5cfc9d4047
7 changed files with 166 additions and 65 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
> [!IMPORTANT]
> **Privacy & Data Sovereignty**: KoalaSync follows a strict "Zero-External-Requests" policy. No fonts, scripts, or assets (images/icons) may be loaded from 3rd-party CDNs or external servers. Everything must be served locally or from the project's own infrastructure.
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
---
+78 -39
View File
@@ -11,6 +11,7 @@ let currentTabId = null;
let currentTabTitle = null; // New: for Smart Matching
let logs = [];
let history = []; // New: for Action History
let reconnectTimer = null;
// Force Sync Coordination
let isForceSyncInitiator = false;
@@ -53,7 +54,12 @@ function addLog(message, type = 'info') {
// --- WebSocket Client ---
async function connect() {
if (isConnecting || (socket && socket.readyState === WebSocket.OPEN)) return;
if (isConnecting) return;
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return;
if (!navigator.onLine) {
addLog('Browser is offline. Waiting...', 'warn');
return;
}
if (!peerId) peerId = await getPeerId();
const settings = await getSettings();
@@ -61,31 +67,52 @@ async function connect() {
isConnecting = true;
broadcastConnectionStatus('connecting');
const isCustomServer = settings.serverUrl && settings.useCustomServer;
const finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
let finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;
// Robustness: Ensure finalUrl is not empty and has a protocol
if (isCustomServer) {
finalUrl = finalUrl.trim();
if (!finalUrl.includes('://')) {
finalUrl = 'ws://' + finalUrl;
}
}
addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
const url = new URL(finalUrl);
url.pathname = '/socket.io/';
url.searchParams.set('EIO', '4');
url.searchParams.set('transport', 'websocket');
url.searchParams.set('version', APP_VERSION);
try {
const url = new URL(finalUrl);
url.pathname = '/socket.io/';
url.searchParams.set('EIO', '4');
url.searchParams.set('transport', 'websocket');
url.searchParams.set('version', APP_VERSION);
if (!isCustomServer) {
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
}
if (!isCustomServer) {
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
} else {
// Self-hosted servers use the same official token by design.
// This allows users to run their own relay while still using
// the official extension — the token is public and not a secret.
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);
}
socket = new WebSocket(url.toString());
socket = new WebSocket(url.toString());
socket.onopen = () => {
socket.onopen = () => {
isConnecting = false;
reconnectDelay = 1000;
addLog('WebSocket Connection Opened', 'success');
broadcastConnectionStatus('connected');
// Socket.IO Handshake: Send "40" to join default namespace
socket.send('40');
};
} catch (e) {
isConnecting = false;
reconnectDelay = 1000;
addLog('WebSocket Connection Opened', 'success');
broadcastConnectionStatus('connected');
// Socket.IO Handshake: Send "40" to join default namespace
socket.send('40');
};
addLog(`Invalid Server URL: ${finalUrl}`, 'error');
broadcastConnectionStatus('disconnected');
scheduleReconnect();
return;
}
socket.onmessage = (event) => {
const msg = event.data;
@@ -169,7 +196,10 @@ function showNotification(senderName, action) {
}
function scheduleReconnect() {
setTimeout(() => {
if (reconnectTimer) return; // Already scheduled
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connect();
}, reconnectDelay);
@@ -182,6 +212,17 @@ function emit(event, data) {
}
}
function addToHistory(action, senderId) {
const historyEntry = {
action,
senderId: senderId || 'You',
timestamp: new Date().toISOString()
};
history.unshift(historyEntry);
if (history.length > 20) history.pop();
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
}
// --- Event Handlers ---
function handleServerEvent(event, data) {
// console.log(`[RECV] ${event}`, data);
@@ -203,7 +244,7 @@ function handleServerEvent(event, data) {
break;
case EVENTS.ERROR:
addLog(`Server Error: ${data.message}`, 'error');
chrome.notifications.create({
chrome.notifications.create(`error_${Date.now()}`, {
type: 'basic',
iconUrl: 'icons/icon128.png',
title: 'KoalaSync Error',
@@ -220,6 +261,10 @@ function handleServerEvent(event, data) {
case EVENTS.PAUSE:
case EVENTS.SEEK:
case EVENTS.FORCE_SYNC_PREPARE:
if (data.senderId) {
addToHistory(event, data.senderId);
showNotification(data.senderId, event);
}
routeToContent(event, data);
break;
case EVENTS.FORCE_SYNC_ACK:
@@ -234,6 +279,10 @@ function handleServerEvent(event, data) {
}
break;
case EVENTS.FORCE_SYNC_EXECUTE:
if (data.senderId) {
addToHistory(event, data.senderId);
showNotification(data.senderId, event);
}
routeToContent(event, data);
break;
case EVENTS.PEER_STATUS:
@@ -263,19 +312,7 @@ function handleServerEvent(event, data) {
}
break;
default:
// History Tracking
const historyEntry = {
action: event,
senderId: data.senderId || 'You',
timestamp: new Date().toISOString()
};
history.unshift(historyEntry);
if (history.length > 20) history.pop();
chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
// Notification for remote actions
if (data.senderId) showNotification(data.senderId, event);
routeToContent(event, data);
addLog(`Received unknown event from server: ${event}`, 'warn');
break;
}
}
@@ -303,8 +340,8 @@ async function routeToContent(action, payload) {
action,
payload
}).catch(err => {
// Auto-Reinject if content script is missing
if (err.message.includes('Receiving end does not exist')) {
// Auto-Reinject if content script is missing or extension was reloaded
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
@@ -335,8 +372,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'CONNECT') {
connect();
} else if (message.type === 'GET_STATUS') {
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting ? 'connecting' : 'disconnected')) : 'disconnected';
sendResponse({ status, peers: currentRoom ? currentRoom.peers : [] });
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting || socket.readyState === WebSocket.CONNECTING ? 'connecting' : 'disconnected')) : 'disconnected';
sendResponse({ status, peerId, peers: currentRoom ? currentRoom.peers : [] });
// Global return true at the end handles this
} else if (message.type === 'LEAVE_ROOM') {
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
@@ -377,6 +415,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
}
}, 5000);
}
addToHistory(message.action, 'You');
emit(message.action, { ...message.payload, peerId });
} else if (message.type === 'FORCE_SYNC_ACK') {
emit(EVENTS.FORCE_SYNC_ACK, { peerId });
@@ -388,7 +427,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Peer status heartbeat from content script
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle });
}
return true;
return true; // Keep channel open for async responses
});
// Tab removal listener
+11 -2
View File
@@ -145,7 +145,8 @@
}
// Heartbeat
setInterval(() => {
let heartbeatErrorCount = 0;
const heartbeatInterval = setInterval(() => {
const video = findVideo();
if (video) {
chrome.runtime.sendMessage({
@@ -154,7 +155,15 @@
playbackState: video.paused ? 'paused' : 'playing',
currentTime: video.currentTime
}
}).catch(() => {});
}).catch(err => {
if (err.message.includes('Extension context invalidated')) {
heartbeatErrorCount++;
if (heartbeatErrorCount === 1) {
console.warn('KoalaSync: Extension reloaded. Please refresh the page if sync stops working.');
}
clearInterval(heartbeatInterval);
}
});
}
}, 15000);
+2 -1
View File
@@ -8,7 +8,8 @@
"tabs",
"scripting",
"alarms",
"activeTab"
"activeTab",
"notifications"
],
"host_permissions": [
"<all_urls>"
+58 -18
View File
@@ -1,8 +1,16 @@
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
const elements = {
tabs: document.querySelectorAll('.tab-btn'),
tabs: document.querySelectorAll('.tabs .tab-btn'),
contents: document.querySelectorAll('.tab-content'),
copyInvite: document.getElementById('copyInvite'),
targetTab: document.getElementById('targetTab'),
@@ -30,6 +38,8 @@ const elements = {
roomError: document.getElementById('roomError')
};
let localPeerId = null;
// --- Initialization ---
async function init() {
// Load Settings
@@ -55,6 +65,7 @@ async function init() {
// Initial Status Check
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
if (res) {
localPeerId = res.peerId;
applyConnectionStatus(res.status);
updatePeerList(res.peers);
}
@@ -78,24 +89,36 @@ function updateUI(roomId, password) {
}
function updatePeerList(peers) {
if (!peers) return;
elements.peerList.innerHTML = peers.map(id => `
<div class="peer-item">
<span>👤 ${id}</span>
</div>
`).join('');
if (!peers || !elements.peerList) return;
elements.peerList.innerHTML = peers.map(p => {
const id = escapeHtml(typeof p === 'object' ? p.peerId : p);
const titleText = (typeof p === 'object' && p.tabTitle) ? escapeHtml(p.tabTitle) : '';
const title = titleText ? `<div style="font-size:10px; color:var(--text-muted);">${titleText}</div>` : '';
return `
<div class="peer-item" style="display:block; padding: 6px 0;">
<div style="display:flex; justify-content:space-between; align-items:center;">
<span style="font-weight:600;">👤 ${id}</span>
${id === escapeHtml(localPeerId) ? '<span style="font-size:10px; color:var(--accent)">YOU</span>' : ''}
</div>
${title}
</div>
`;
}).join('');
// Re-populate tabs to update Star Matching when peers change
populateTabs();
populateTabs(peers);
}
async function populateTabs() {
async function populateTabs(providedPeers = null) {
const data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']);
const isFilterActive = data.filterNoise !== false;
const currentTargetTabId = data.targetTabId;
// Get current peers from background to do matching
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
const peerIds = status?.peers || [];
// Use provided peers or fetch if missing
let peerIds = providedPeers;
if (!peerIds) {
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
peerIds = status?.peers || [];
}
const tabs = await chrome.tabs.query({});
@@ -165,8 +188,9 @@ function updateHistory(history) {
}
elements.historyList.innerHTML = history.map(item => {
const time = new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const actionLabel = item.action.toUpperCase().replace('FORCE_SYNC_', '');
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : item.senderId;
const actionLabel = escapeHtml(item.action.toUpperCase().replace('FORCE_SYNC_', ''));
const senderIdEscaped = escapeHtml(item.senderId);
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : senderIdEscaped;
return `<div style="margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px;">
<span style="color:#64748b">[${time}]</span> <b>${actionLabel}</b> by ${sender}
</div>`;
@@ -185,9 +209,12 @@ function updateRoomList(rooms) {
return;
}
elements.publicRooms.innerHTML = rooms.map(r => `
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${r.id}">
<span style="font-weight:600;">${r.id}</span>
<span style="font-size:11px; color:var(--accent)">${r.peerCount} peers</span>
<div class="room-item" style="display:flex; justify-content:space-between; align-items:center; padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.05); cursor:pointer;" data-id="${escapeHtml(r.id)}">
<div style="display:flex; align-items:center; gap: 6px;">
<span style="font-weight:600;">${escapeHtml(r.id)}</span>
${r.hasPassword ? '<span title="Password Protected">🔒</span>' : ''}
</div>
<span style="font-size:11px; color:var(--accent)">${parseInt(r.peerCount)} peers</span>
</div>
`).join('');
@@ -232,6 +259,19 @@ elements.filterNoise.addEventListener('change', () => {
});
});
elements.serverUrl.addEventListener('input', () => {
chrome.storage.sync.set({ serverUrl: elements.serverUrl.value });
});
elements.serverUrl.addEventListener('change', () => {
let url = elements.serverUrl.value.trim();
if (url && !url.includes('://')) {
url = 'ws://' + url;
elements.serverUrl.value = url;
chrome.storage.sync.set({ serverUrl: url });
}
});
elements.tabs.forEach(btn => {
btn.addEventListener('click', () => {
elements.tabs.forEach(b => b.classList.remove('active'));
+14 -3
View File
@@ -178,6 +178,7 @@ io.on('connection', (socket) => {
passwordHash,
peers: new Set(),
peerIds: new Map(),
peerData: new Map(), // socketId -> { peerId, tabTitle }
lastActivity: Date.now()
};
rooms.set(roomId, room);
@@ -199,12 +200,13 @@ io.on('connection', (socket) => {
socket.join(roomId);
room.peers.add(socket.id);
room.peerIds.set(socket.id, peerId);
room.peerData.set(socket.id, { peerId, tabTitle: null });
socketToRoom.set(socket.id, { roomId, peerId });
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' });
socket.emit(EVENTS.ROOM_DATA, {
roomId,
peers: Array.from(room.peers).map(sid => room.peerIds.get(sid))
peers: Array.from(room.peers).map(sid => room.peerData.get(sid))
});
log('ROOM', `Peer ${peerId} joined: ${roomId}`);
} catch (err) {
@@ -231,7 +233,13 @@ io.on('connection', (socket) => {
const mapping = socketToRoom.get(socket.id);
if (mapping) {
const room = rooms.get(mapping.roomId);
if (room) room.lastActivity = Date.now();
if (room) {
room.lastActivity = Date.now();
// Update metadata if it's a peer_status (heartbeat)
if (eventName === EVENTS.PEER_STATUS && data.tabTitle) {
room.peerData.set(socket.id, { peerId: mapping.peerId, tabTitle: data.tabTitle });
}
}
socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
}
});
@@ -240,7 +248,8 @@ io.on('connection', (socket) => {
socket.on(EVENTS.GET_ROOMS, () => {
const list = Array.from(rooms.entries()).map(([id, r]) => ({
id,
peerCount: r.peers.size
peerCount: r.peers.size,
hasPassword: !!r.passwordHash
}));
socket.emit(EVENTS.ROOM_LIST, { rooms: list });
});
@@ -254,6 +263,7 @@ io.on('connection', (socket) => {
if (room) {
room.peers.delete(socket.id);
room.peerIds.delete(socket.id);
room.peerData.delete(socket.id);
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
if (room.peers.size === 0) {
rooms.delete(roomId);
@@ -273,6 +283,7 @@ io.on('connection', (socket) => {
if (room) {
room.peers.delete(socket.id);
room.peerIds.delete(socket.id);
room.peerData.delete(socket.id);
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
if (room.peers.size === 0) {
rooms.delete(roomId);
+1 -1
View File
@@ -7,7 +7,7 @@ export const APP_VERSION = "1.0.0";
export const OFFICIAL_SERVER_URL = 'wss://sync.shik3i.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.shik3i.net';
export const OFFICIAL_SERVER_TOKEN = 'koala_secure_access_2026';
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
export const EVENTS = {
// Connection & Room