Skip to content

sync#3

Draft
bart-jaskulski wants to merge 1 commit intofeat/dndfrom
feat/sync
Draft

sync#3
bart-jaskulski wants to merge 1 commit intofeat/dndfrom
feat/sync

Conversation

@bart-jaskulski
Copy link
Owner

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a complete end-to-end encrypted synchronization system for the task management application, enabling real-time collaboration across multiple devices through a blind relay server.

Key changes:

  • Migrated WebSocket handler from standalone route (src/ws.ts) to API route (src/routes/api/ws.ts) with enhanced error logging and message type handling
  • Extended task store with device management, session bootstrapping, and encrypted WebSocket provider integration for real-time sync
  • Added comprehensive settings UI for pairing link management, device tracking, and room rotation

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/ws.ts Removed standalone WebSocket handler (moved to API routes)
src/routes/api/ws.ts New WebSocket API endpoint with persistent storage, history streaming, error logging, and room-based pub/sub
src/stores/taskStore.ts Added device tracking, session management, encrypted sync provider integration, heartbeat mechanism, and pairing utilities
src/routes/settings.tsx New settings page UI for sync configuration, pairing link sharing, device management, and room rotation
src/routes/settings.css Styles for settings page with responsive grid layout and device card components
src/lib/encryptedWsProvider.ts Enhanced with status callbacks, snapshot push capability, and improved connection logging
app.config.ts Removed custom router configuration, added vite server allowed hosts setting

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

type="text"
readOnly
value={shareLink()}
aria-label="Pairing link"
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

[nitpick] The pairing link input field is read-only but doesn't have autocomplete="off" or spellcheck="false" attributes. While cosmetic, this would prevent the browser from trying to autocomplete or spellcheck the encrypted URL, improving UX.

Suggested change
aria-label="Pairing link"
aria-label="Pairing link"
autocomplete="off"
spellcheck="false"

Copilot uses AI. Check for mistakes.
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
const stack = err instanceof Error && err.stack ? err.stack : "";
try {
await appendFile("./storage/ws-errors.log", `[${new Date().toISOString()}] ${scope} :: ${message}\n${stack}\n`);
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The error log file path is hardcoded to "./storage/ws-errors.log" which may not work correctly depending on the working directory at runtime. Consider using an absolute path based on process.env.LOG_PATH or ensuring the directory exists before writing.

Copilot uses AI. Check for mistakes.
const query = getQuery(peer.url);
const roomId = query.room as string;

if (!roomId) {
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The roomId query parameter is not validated or sanitized. A malicious value could potentially cause issues with the database key construction (e.g., containing :: separators). Consider validating that roomId matches an expected format (e.g., UUID pattern) before using it.

Suggested change
if (!roomId) {
// Validate roomId as UUID v4
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!roomId || !uuidV4Regex.test(roomId)) {

Copilot uses AI. Check for mistakes.
// Auto-initialize immediately when this file is imported on the client
onMount(init);
if (!isServer) {
void init();
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The init function is called immediately with void init() on line 210, but the function is declared as async on line 173. This means initialization errors will be silently swallowed. Consider adding a .catch() handler to log initialization failures: void init().catch(err => console.error("Init failed:", err)).

Suggested change
void init();
void init().catch(err => console.error("Init failed:", err));

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +115
const platform = (navigator as any).userAgentData?.platform ?? navigator.platform ?? "device";
const hint = navigator.userAgent.split(" ").slice(0, 2).join(" ");
return `${platform} · ${hint}`;
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The user agent string parsing in defaultDeviceLabel may fail or produce unexpected results on non-standard browsers. The fallback chain is good, but consider wrapping the entire computation in a try-catch to ensure it always returns a valid string even if navigator.userAgent.split() throws.

Suggested change
const platform = (navigator as any).userAgentData?.platform ?? navigator.platform ?? "device";
const hint = navigator.userAgent.split(" ").slice(0, 2).join(" ");
return `${platform} · ${hint}`;
try {
const platform = (navigator as any).userAgentData?.platform ?? navigator.platform ?? "device";
const hint = navigator.userAgent.split(" ").slice(0, 2).join(" ");
return `${platform} · ${hint}`;
} catch {
return "device";
}

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +15
const logError = async (scope: string, err: unknown) => {
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
const stack = err instanceof Error && err.stack ? err.stack : "";
try {
await appendFile("./storage/ws-errors.log", `[${new Date().toISOString()}] ${scope} :: ${message}\n${stack}\n`);
} catch (fileErr) {
console.error("[WS] Failed to write debug log", fileErr);
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

[nitpick] Using void to suppress the promise from logError is appropriate, but the function should handle the file write error more gracefully. Currently, if appendFile fails, the error is only logged to console. Consider implementing a fallback logging mechanism or at least incrementing a counter to track lost error logs.

Suggested change
const logError = async (scope: string, err: unknown) => {
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
const stack = err instanceof Error && err.stack ? err.stack : "";
try {
await appendFile("./storage/ws-errors.log", `[${new Date().toISOString()}] ${scope} :: ${message}\n${stack}\n`);
} catch (fileErr) {
console.error("[WS] Failed to write debug log", fileErr);
// Counter for lost error logs
let lostErrorLogCount = 0;
const logError = async (scope: string, err: unknown) => {
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
const stack = err instanceof Error && err.stack ? err.stack : "";
try {
await appendFile("./storage/ws-errors.log", `[${new Date().toISOString()}] ${scope} :: ${message}\n${stack}\n`);
} catch (fileErr) {
lostErrorLogCount++;
console.error("[WS] Failed to write debug log", fileErr, `Lost error logs: ${lostErrorLogCount}`);

Copilot uses AI. Check for mistakes.
},
vite: {
server: {
allowedHosts: true
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

Inconsistent indentation: line 11 has only 4 spaces while the surrounding lines use proper indentation. This should be indented to align with the opening of the server object property.

Suggested change
allowedHosts: true
allowedHosts: true

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +49
(async () => {
for await (const [_, value] of stream) {
const payload = Buffer.isBuffer(value) ? value : Buffer.from(value);
peer.send(payload);
}
})().catch((err) => {
console.error("[WS] History stream error", err);
void logError("history", err);
peer.close(1011, "history-error");
});
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The asynchronous IIFE for streaming history lacks proper error handling until caught at line 45. If an error occurs during iteration (lines 41-44), it will only be caught after the IIFE completes, potentially leaving the peer in an inconsistent state. Consider wrapping the send operation in a try-catch to handle individual message failures gracefully.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +150
if (typeof window !== "undefined" && window.location.hash.includes("room=")) {
const params = new URLSearchParams(window.location.hash.slice(1));
const hashRoom = params.get("room");
const hashKey = params.get("key");
if (hashRoom && hashKey) {
roomId = hashRoom;
rawKey = hashKey;
localStorage.setItem(STORAGE_KEYS.room, hashRoom);
localStorage.setItem(STORAGE_KEYS.key, hashKey);
window.history.replaceState(null, "", window.location.pathname + window.location.search);
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The hash parsing logic extracts credentials from the URL hash but doesn't validate the format of the key parameter. Consider validating that the key matches the expected format/length before storing it to localStorage, to avoid storing malformed credentials.

Copilot uses AI. Check for mistakes.
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const host = window.location.host;
this.url = `${protocol}//${host}${serverUrl}?room=${roomId}`;
console.log("[WS] Connecting to:", this.url);
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

[nitpick] The WebSocket URL is logged to console, which will include the roomId query parameter. While the encryption key is not in the URL, the roomId might be considered sensitive. Consider using a debug flag to control this logging or redacting the room parameter in production.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant