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. 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] > [!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 currentTabTitle = null; // New: for Smart Matching
let logs = []; let logs = [];
let history = []; // New: for Action History let history = []; // New: for Action History
let reconnectTimer = null;
// Force Sync Coordination // Force Sync Coordination
let isForceSyncInitiator = false; let isForceSyncInitiator = false;
@@ -53,7 +54,12 @@ function addLog(message, type = 'info') {
// --- WebSocket Client --- // --- WebSocket Client ---
async function connect() { 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(); if (!peerId) peerId = await getPeerId();
const settings = await getSettings(); const settings = await getSettings();
@@ -61,31 +67,52 @@ async function connect() {
isConnecting = true; isConnecting = true;
broadcastConnectionStatus('connecting'); broadcastConnectionStatus('connecting');
const isCustomServer = settings.serverUrl && settings.useCustomServer; 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'); addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}...`, 'info');
const url = new URL(finalUrl); try {
url.pathname = '/socket.io/'; const url = new URL(finalUrl);
url.searchParams.set('EIO', '4'); url.pathname = '/socket.io/';
url.searchParams.set('transport', 'websocket'); url.searchParams.set('EIO', '4');
url.searchParams.set('version', APP_VERSION); url.searchParams.set('transport', 'websocket');
url.searchParams.set('version', APP_VERSION);
if (!isCustomServer) { if (!isCustomServer) {
url.searchParams.set('token', OFFICIAL_SERVER_TOKEN); 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; isConnecting = false;
reconnectDelay = 1000; addLog(`Invalid Server URL: ${finalUrl}`, 'error');
addLog('WebSocket Connection Opened', 'success'); broadcastConnectionStatus('disconnected');
broadcastConnectionStatus('connected'); scheduleReconnect();
return;
// Socket.IO Handshake: Send "40" to join default namespace }
socket.send('40');
};
socket.onmessage = (event) => { socket.onmessage = (event) => {
const msg = event.data; const msg = event.data;
@@ -169,7 +196,10 @@ function showNotification(senderName, action) {
} }
function scheduleReconnect() { function scheduleReconnect() {
setTimeout(() => { if (reconnectTimer) return; // Already scheduled
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connect(); connect();
}, reconnectDelay); }, 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 --- // --- Event Handlers ---
function handleServerEvent(event, data) { function handleServerEvent(event, data) {
// console.log(`[RECV] ${event}`, data); // console.log(`[RECV] ${event}`, data);
@@ -203,7 +244,7 @@ function handleServerEvent(event, data) {
break; break;
case EVENTS.ERROR: case EVENTS.ERROR:
addLog(`Server Error: ${data.message}`, 'error'); addLog(`Server Error: ${data.message}`, 'error');
chrome.notifications.create({ chrome.notifications.create(`error_${Date.now()}`, {
type: 'basic', type: 'basic',
iconUrl: 'icons/icon128.png', iconUrl: 'icons/icon128.png',
title: 'KoalaSync Error', title: 'KoalaSync Error',
@@ -220,6 +261,10 @@ function handleServerEvent(event, data) {
case EVENTS.PAUSE: case EVENTS.PAUSE:
case EVENTS.SEEK: case EVENTS.SEEK:
case EVENTS.FORCE_SYNC_PREPARE: case EVENTS.FORCE_SYNC_PREPARE:
if (data.senderId) {
addToHistory(event, data.senderId);
showNotification(data.senderId, event);
}
routeToContent(event, data); routeToContent(event, data);
break; break;
case EVENTS.FORCE_SYNC_ACK: case EVENTS.FORCE_SYNC_ACK:
@@ -234,6 +279,10 @@ function handleServerEvent(event, data) {
} }
break; break;
case EVENTS.FORCE_SYNC_EXECUTE: case EVENTS.FORCE_SYNC_EXECUTE:
if (data.senderId) {
addToHistory(event, data.senderId);
showNotification(data.senderId, event);
}
routeToContent(event, data); routeToContent(event, data);
break; break;
case EVENTS.PEER_STATUS: case EVENTS.PEER_STATUS:
@@ -263,19 +312,7 @@ function handleServerEvent(event, data) {
} }
break; break;
default: default:
// History Tracking addLog(`Received unknown event from server: ${event}`, 'warn');
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);
break; break;
} }
} }
@@ -303,8 +340,8 @@ async function routeToContent(action, payload) {
action, action,
payload payload
}).catch(err => { }).catch(err => {
// Auto-Reinject if content script is missing // Auto-Reinject if content script is missing or extension was reloaded
if (err.message.includes('Receiving end does not exist')) { if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
chrome.scripting.executeScript({ chrome.scripting.executeScript({
target: { tabId }, target: { tabId },
files: ['content.js'] files: ['content.js']
@@ -335,8 +372,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'CONNECT') { if (message.type === 'CONNECT') {
connect(); connect();
} else if (message.type === 'GET_STATUS') { } else if (message.type === 'GET_STATUS') {
const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting ? 'connecting' : 'disconnected')) : 'disconnected'; const status = socket ? (socket.readyState === WebSocket.OPEN ? 'connected' : (isConnecting || socket.readyState === WebSocket.CONNECTING ? 'connecting' : 'disconnected')) : 'disconnected';
sendResponse({ status, peers: currentRoom ? currentRoom.peers : [] }); sendResponse({ status, peerId, peers: currentRoom ? currentRoom.peers : [] });
// Global return true at the end handles this
} else if (message.type === 'LEAVE_ROOM') { } else if (message.type === 'LEAVE_ROOM') {
emit(EVENTS.LEAVE_ROOM, { peerId }); emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null; currentRoom = null;
@@ -377,6 +415,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
} }
}, 5000); }, 5000);
} }
addToHistory(message.action, 'You');
emit(message.action, { ...message.payload, peerId }); emit(message.action, { ...message.payload, peerId });
} else if (message.type === 'FORCE_SYNC_ACK') { } else if (message.type === 'FORCE_SYNC_ACK') {
emit(EVENTS.FORCE_SYNC_ACK, { peerId }); emit(EVENTS.FORCE_SYNC_ACK, { peerId });
@@ -388,7 +427,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Peer status heartbeat from content script // Peer status heartbeat from content script
emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle }); emit(EVENTS.PEER_STATUS, { ...message.payload, peerId, tabTitle: currentTabTitle });
} }
return true; return true; // Keep channel open for async responses
}); });
// Tab removal listener // Tab removal listener
+11 -2
View File
@@ -145,7 +145,8 @@
} }
// Heartbeat // Heartbeat
setInterval(() => { let heartbeatErrorCount = 0;
const heartbeatInterval = setInterval(() => {
const video = findVideo(); const video = findVideo();
if (video) { if (video) {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
@@ -154,7 +155,15 @@
playbackState: video.paused ? 'paused' : 'playing', playbackState: video.paused ? 'paused' : 'playing',
currentTime: video.currentTime 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); }, 15000);
+2 -1
View File
@@ -8,7 +8,8 @@
"tabs", "tabs",
"scripting", "scripting",
"alarms", "alarms",
"activeTab" "activeTab",
"notifications"
], ],
"host_permissions": [ "host_permissions": [
"<all_urls>" "<all_urls>"
+58 -18
View File
@@ -1,8 +1,16 @@
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js'; import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.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 = { const elements = {
tabs: document.querySelectorAll('.tab-btn'), tabs: document.querySelectorAll('.tabs .tab-btn'),
contents: document.querySelectorAll('.tab-content'), contents: document.querySelectorAll('.tab-content'),
copyInvite: document.getElementById('copyInvite'), copyInvite: document.getElementById('copyInvite'),
targetTab: document.getElementById('targetTab'), targetTab: document.getElementById('targetTab'),
@@ -30,6 +38,8 @@ const elements = {
roomError: document.getElementById('roomError') roomError: document.getElementById('roomError')
}; };
let localPeerId = null;
// --- Initialization --- // --- Initialization ---
async function init() { async function init() {
// Load Settings // Load Settings
@@ -55,6 +65,7 @@ async function init() {
// Initial Status Check // Initial Status Check
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => { chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (res) => {
if (res) { if (res) {
localPeerId = res.peerId;
applyConnectionStatus(res.status); applyConnectionStatus(res.status);
updatePeerList(res.peers); updatePeerList(res.peers);
} }
@@ -78,24 +89,36 @@ function updateUI(roomId, password) {
} }
function updatePeerList(peers) { function updatePeerList(peers) {
if (!peers) return; if (!peers || !elements.peerList) return;
elements.peerList.innerHTML = peers.map(id => ` elements.peerList.innerHTML = peers.map(p => {
<div class="peer-item"> const id = escapeHtml(typeof p === 'object' ? p.peerId : p);
<span>👤 ${id}</span> const titleText = (typeof p === 'object' && p.tabTitle) ? escapeHtml(p.tabTitle) : '';
</div> const title = titleText ? `<div style="font-size:10px; color:var(--text-muted);">${titleText}</div>` : '';
`).join(''); 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 // 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 data = await chrome.storage.sync.get(['targetTabId', 'filterNoise']);
const isFilterActive = data.filterNoise !== false; const isFilterActive = data.filterNoise !== false;
const currentTargetTabId = data.targetTabId; const currentTargetTabId = data.targetTabId;
// Get current peers from background to do matching // Use provided peers or fetch if missing
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r)); let peerIds = providedPeers;
const peerIds = status?.peers || []; if (!peerIds) {
const status = await new Promise(r => chrome.runtime.sendMessage({ type: 'GET_STATUS' }, r));
peerIds = status?.peers || [];
}
const tabs = await chrome.tabs.query({}); const tabs = await chrome.tabs.query({});
@@ -165,8 +188,9 @@ function updateHistory(history) {
} }
elements.historyList.innerHTML = history.map(item => { elements.historyList.innerHTML = history.map(item => {
const time = new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); 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 actionLabel = escapeHtml(item.action.toUpperCase().replace('FORCE_SYNC_', ''));
const sender = item.senderId === 'You' ? '<span style="color:var(--accent)">You</span>' : item.senderId; 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;"> 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} <span style="color:#64748b">[${time}]</span> <b>${actionLabel}</b> by ${sender}
</div>`; </div>`;
@@ -185,9 +209,12 @@ function updateRoomList(rooms) {
return; return;
} }
elements.publicRooms.innerHTML = rooms.map(r => ` 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}"> <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)}">
<span style="font-weight:600;">${r.id}</span> <div style="display:flex; align-items:center; gap: 6px;">
<span style="font-size:11px; color:var(--accent)">${r.peerCount} peers</span> <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> </div>
`).join(''); `).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 => { elements.tabs.forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
elements.tabs.forEach(b => b.classList.remove('active')); elements.tabs.forEach(b => b.classList.remove('active'));
+14 -3
View File
@@ -178,6 +178,7 @@ io.on('connection', (socket) => {
passwordHash, passwordHash,
peers: new Set(), peers: new Set(),
peerIds: new Map(), peerIds: new Map(),
peerData: new Map(), // socketId -> { peerId, tabTitle }
lastActivity: Date.now() lastActivity: Date.now()
}; };
rooms.set(roomId, room); rooms.set(roomId, room);
@@ -199,12 +200,13 @@ io.on('connection', (socket) => {
socket.join(roomId); socket.join(roomId);
room.peers.add(socket.id); room.peers.add(socket.id);
room.peerIds.set(socket.id, peerId); room.peerIds.set(socket.id, peerId);
room.peerData.set(socket.id, { peerId, tabTitle: null });
socketToRoom.set(socket.id, { roomId, peerId }); socketToRoom.set(socket.id, { roomId, peerId });
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' }); socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'joined' });
socket.emit(EVENTS.ROOM_DATA, { socket.emit(EVENTS.ROOM_DATA, {
roomId, 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}`); log('ROOM', `Peer ${peerId} joined: ${roomId}`);
} catch (err) { } catch (err) {
@@ -231,7 +233,13 @@ io.on('connection', (socket) => {
const mapping = socketToRoom.get(socket.id); const mapping = socketToRoom.get(socket.id);
if (mapping) { if (mapping) {
const room = rooms.get(mapping.roomId); 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 }); socket.to(mapping.roomId).emit(eventName, { ...data, senderId: mapping.peerId });
} }
}); });
@@ -240,7 +248,8 @@ io.on('connection', (socket) => {
socket.on(EVENTS.GET_ROOMS, () => { socket.on(EVENTS.GET_ROOMS, () => {
const list = Array.from(rooms.entries()).map(([id, r]) => ({ const list = Array.from(rooms.entries()).map(([id, r]) => ({
id, id,
peerCount: r.peers.size peerCount: r.peers.size,
hasPassword: !!r.passwordHash
})); }));
socket.emit(EVENTS.ROOM_LIST, { rooms: list }); socket.emit(EVENTS.ROOM_LIST, { rooms: list });
}); });
@@ -254,6 +263,7 @@ io.on('connection', (socket) => {
if (room) { if (room) {
room.peers.delete(socket.id); room.peers.delete(socket.id);
room.peerIds.delete(socket.id); room.peerIds.delete(socket.id);
room.peerData.delete(socket.id);
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' }); socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
if (room.peers.size === 0) { if (room.peers.size === 0) {
rooms.delete(roomId); rooms.delete(roomId);
@@ -273,6 +283,7 @@ io.on('connection', (socket) => {
if (room) { if (room) {
room.peers.delete(socket.id); room.peers.delete(socket.id);
room.peerIds.delete(socket.id); room.peerIds.delete(socket.id);
room.peerData.delete(socket.id);
socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' }); socket.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
if (room.peers.size === 0) { if (room.peers.size === 0) {
rooms.delete(roomId); 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_SERVER_URL = 'wss://sync.shik3i.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://koalasync.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 = { export const EVENTS = {
// Connection & Room // Connection & Room