Skip to content

Conversation

Copy link

Copilot AI commented Nov 30, 2025

Adds the ability to sync UUID UI history and favorites to GitHub Gist, enabling data restoration after browser resets and cross-device access without a custom backend.

Changes

  • src/gist-sync.js - Gist API service handling create/update/read operations, token management, and data serialization
  • src/gist-sync-modal.js - Modal UI for token entry, sync/restore actions, and connection status
  • src/nav.js - Sync button in nav bar with connected state indicator (green when authenticated)
  • src/app.js - Added restoreFromGist handler to replace local state with gist data
  • tests/gist-sync.js - Unit tests for serialization logic

Usage

  1. Click the cloud icon in the nav bar
  2. Create a GitHub PAT with gist scope (link provided in modal)
  3. Connect and use "Sync to Gist" / "Restore from Gist"

Data is stored in a private gist as uuid-ui-data.json with items capped at 100 entries.

Gist Sync Modal

Original prompt

This section details on the original issue you should resolve

<issue_title>gist sync</issue_title>
<issue_description>Add an option to sync uuid-ui data across browsers and machines using the GitHub Gist API. This would also allow restoring data after a browser reset without needing a custom backend.</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add option to sync uuid-ui data using GitHub Gist API Add GitHub Gist sync for cross-browser/machine data persistence Nov 30, 2025
Copilot AI requested a review from rez1dent3 November 30, 2025 13:46
Copilot finished work on behalf of rez1dent3 November 30, 2025 13:46
@rez1dent3 rez1dent3 requested a review from Copilot November 30, 2025 21:45
Copilot finished reviewing on behalf of rez1dent3 November 30, 2025 21:47
Copy link
Contributor

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 adds GitHub Gist sync functionality to enable cross-browser and cross-machine data persistence for UUID UI history and favorites. Users can authenticate with a GitHub Personal Access Token (PAT), sync their local data to a private gist, and restore it on other devices or after browser resets.

Key Changes:

  • New Gist API integration service with create/update/read operations and token management
  • Modal UI component for authentication and sync/restore actions
  • Integration with existing app state management for data restoration

Reviewed changes

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

Show a summary per file
File Description
src/gist-sync.js Core service implementing GitHub Gist API operations, data serialization (capped at 100 items), and localStorage-based token management
src/gist-sync-modal.js Modal component providing UI for token authentication, sync/restore operations, and connection status with visual feedback
src/nav.js Adds cloud sync button to navigation bar with green indicator when authenticated, integrates modal component
src/app.js Implements restoreFromGist handler to deserialize and restore items/favorites from gist data
tests/gist-sync.js Unit tests for data serialization logic including item limiting and empty data handling
.gitignore Updated to exclude build artifacts in public/assets/
public/index.html Updated bundle references (build artifacts that should be gitignored)
public/assets/index-lWTPcmWB.css Build artifact marked for deletion

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

Comment on lines +38 to +39
}
}, [isOpen]);
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

[nitpick] In the useEffect hook, the isOpen state is checked but the effect doesn't reset the state when the modal is closed. This means that if a user opens the modal, enters a token but doesn't connect, then closes and reopens the modal, the unsaved token will still be visible in the input field. Consider resetting the token state when isOpen becomes false and the user is not connected:

useEffect(() => {
  if (isOpen) {
    const savedToken = getGistToken();
    if (savedToken) {
      setToken(savedToken);
      setIsConnected(true);
    }
    const savedLastSync = getLastSyncTimestamp();
    if (savedLastSync) {
      setLastSync(savedLastSync);
    }
  } else if (!isConnected) {
    // Reset unsaved token when modal closes
    setToken('');
  }
}, [isOpen, isConnected]);
Suggested change
}
}, [isOpen]);
} else if (!isConnected) {
// Reset unsaved token when modal closes
setToken('');
}
}, [isOpen, isConnected]);

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +13
<script type="module" crossorigin src="./assets/bundle-DScjgMdC.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B5HI90Nx.css">
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The generated bundle files in public/assets/ should not be committed to version control as they are build artifacts. The .gitignore has been correctly updated to exclude public/assets/, but these files (index-lWTPcmWB.css and the HTML references to bundle files) are still being tracked in this PR. These should be removed from the commit.

Suggested change
<script type="module" crossorigin src="./assets/bundle-DScjgMdC.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-B5HI90Nx.css">

Copilot uses AI. Check for mistakes.
}
}

this.setState({ items, favorites }, () => {
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The restoreFromGist method doesn't save the restored items to localStorage. This creates an inconsistency where restored data won't persist in localStorage until the next item is added or modified via componentDidUpdate. Users could lose their restored data if they close the browser before making any changes.

Consider calling localStorage.setItem('uuidItems', JSON.stringify(...)) after updating the state, similar to the pattern used in componentDidUpdate.

Suggested change
this.setState({ items, favorites }, () => {
this.setState({ items, favorites }, () => {
// Save restored items to localStorage
try {
localStorage.setItem('uuidItems', JSON.stringify(
items.map(item => ({
input: item.input || '',
output: item.output || '',
info: item.info || ''
}))
));
} catch (e) {
console.error('Error saving restored items to localStorage:', e);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +69
import assert from 'node:assert';
import test from 'node:test';
import { GistSync } from '../src/gist-sync.js';

test('GistSync.serializeData - serializes items and favorites correctly', () => {
const gistSync = new GistSync('test-token');

const items = [
{ input: '0;1', output: 'uuid1', info: 'comment1' },
{ input: 'uuid2', output: '0;2', info: '' }
];

const favorites = {
'myList': [
{ input: 'uuid3', output: '0;3', info: 'fav comment' }
]
};

const result = gistSync.serializeData(items, favorites);

assert.strictEqual(result.version, 1);
assert.ok(result.timestamp);
assert.strictEqual(result.items.length, 2);
assert.strictEqual(result.items[0].input, '0;1');
assert.strictEqual(result.items[0].output, 'uuid1');
assert.strictEqual(result.items[0].info, 'comment1');
assert.strictEqual(result.items[1].input, 'uuid2');
assert.strictEqual(result.items[1].output, '0;2');
assert.strictEqual(result.items[1].info, '');
assert.ok(result.favorites.myList);
assert.strictEqual(result.favorites.myList.length, 1);
assert.strictEqual(result.favorites.myList[0].input, 'uuid3');
});

test('GistSync.serializeData - handles empty data', () => {
const gistSync = new GistSync('test-token');

const result = gistSync.serializeData([], {});

assert.strictEqual(result.version, 1);
assert.ok(result.timestamp);
assert.strictEqual(result.items.length, 0);
assert.deepStrictEqual(result.favorites, {});
});

test('GistSync.serializeData - limits items to 100', () => {
const gistSync = new GistSync('test-token');

const items = Array(150).fill(null).map((_, i) => ({
input: `input${i}`,
output: `output${i}`,
info: ''
}));

const result = gistSync.serializeData(items, {});

assert.strictEqual(result.items.length, 100);
});

test('GistSync.getHeaders - returns correct headers', () => {
const gistSync = new GistSync('test-token-123');

const headers = gistSync.getHeaders();

assert.strictEqual(headers['Authorization'], 'Bearer test-token-123');
assert.strictEqual(headers['Accept'], 'application/vnd.github+json');
assert.strictEqual(headers['Content-Type'], 'application/json');
assert.strictEqual(headers['X-GitHub-Api-Version'], '2022-11-28');
});
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The test suite only covers the synchronous serializeData and getHeaders methods, but doesn't test the core async functionality like syncToGist, restoreFromGist, findExistingGist, createGist, updateGist, and validateToken. Since the repository has comprehensive test coverage for other modules (base64, bytes, high-low, type-detector, ulid), the async API methods should also have test coverage using mocked fetch calls.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
const handleDisconnect = () => {
removeGistToken();
setToken('');
setIsConnected(false);
setLastSync(null);
toast.success('Disconnected from GitHub');
};
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

When disconnecting from GitHub, the last sync timestamp is cleared from the UI state but not from localStorage. This means that after disconnecting and reconnecting with the same or different token, the old sync timestamp will reappear, which is misleading since it represents a sync from the previous connection.

The handleDisconnect function should also call localStorage.removeItem('gistLastSync') to ensure consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +69
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to fetch gists: ${response.status}`);
}
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The error handling for findExistingGist uses response.json().catch(() => ({})) which suppresses JSON parsing errors. If the response body is not valid JSON, the error message will be generic. A more informative approach would be to handle JSON parsing errors separately or provide a fallback error message that doesn't rely on parsing the error response body.

For example:

if (!response.ok) {
  let errorMessage = `Failed to fetch gists: ${response.status}`;
  try {
    const error = await response.json();
    if (error.message) errorMessage = error.message;
  } catch {
    // Keep the default error message
  }
  throw new Error(errorMessage);
}

Copilot uses AI. Check for mistakes.
Comment on lines +209 to +210
} catch {
return false;
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

The validateToken method silently returns false on network or other errors, making it difficult to distinguish between an invalid token and network issues. This could confuse users who have a valid token but experience temporary network problems. Consider propagating the actual error or providing more specific error handling to differentiate between authentication failures and network errors.

Suggested change
} catch {
return false;
} catch (error) {
throw error;

Copilot uses AI. Check for mistakes.
Comment on lines +106 to +136
const handleRestore = async () => {
const savedToken = getGistToken();
if (!savedToken) {
toast.error('Not connected to GitHub');
return;
}

setIsRestoring(true);
try {
const gistSync = new GistSync(savedToken);
const data = await gistSync.restoreFromGist();

if (!data) {
toast.info('No data found', {
description: 'No UUID UI gist found in your account'
});
return;
}

onRestore(data.items, data.favorites);
toast.success('Data restored', {
description: `Restored ${data.items.length} items from gist`
});
} catch (error) {
toast.error('Restore failed', {
description: error.message
});
} finally {
setIsRestoring(false);
}
};
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

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

[nitpick] The handleRestore function doesn't close the modal after successfully restoring data. Users might expect the modal to close automatically after a successful restore operation, similar to how many UI patterns work. Consider adding onClose() after the success toast on line 128, or provide a clear indication that users should manually close the modal.

Copilot uses AI. Check for mistakes.
@rez1dent3 rez1dent3 requested a review from Copilot December 1, 2025 17:05
Copilot finished reviewing on behalf of rez1dent3 December 1, 2025 17:08
Copy link
Contributor

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

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


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


const result = gistSync.serializeData(items, {});

assert.strictEqual(result.items.length, 100);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The test should verify that items beyond position 100 are actually excluded, not just that the result has 100 items. The first 100 items might be included by coincidence. Add an assertion to check the content:

assert.strictEqual(result.items.length, 100);
assert.strictEqual(result.items[0].input, 'input0');
assert.strictEqual(result.items[99].input, 'input99');
// Verify that item 100+ are not included
assert.ok(!result.items.find(item => item.input === 'input100'));
Suggested change
assert.strictEqual(result.items.length, 100);
assert.strictEqual(result.items.length, 100);
assert.strictEqual(result.items[0].input, 'input0');
assert.strictEqual(result.items[99].input, 'input99');
assert.ok(!result.items.find(item => item.input === 'input100'));

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +196
<button
onClick={onClose}
className={`p-2 rounded-lg ${isToggled ? 'hover:bg-gray-700 text-gray-400 hover:text-gray-300' : 'hover:bg-gray-100 text-gray-400 hover:text-gray-600'} transition-colors`}
aria-label="Close"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Accessibility issue: The modal close button (X) at the top right should have keyboard support. Users should be able to close the modal with the Escape key. Add a keyboard event listener:

useEffect(() => {
    if (!isOpen) return;
    
    const handleEscape = (e) => {
        if (e.key === 'Escape') {
            onClose();
        }
    };
    
    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, onClose]);

Copilot uses AI. Check for mistakes.
}

this.setState({ items, favorites }, () => {
this.saveFavoritesToStorage(favorites);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing error handling for saveFavoritesToStorage. If this method throws an error, the state will be updated but favorites won't be persisted to storage, leading to data inconsistency. Consider wrapping the callback in a try-catch block:

this.setState({ items, favorites }, () => {
    try {
        this.saveFavoritesToStorage(favorites);
    } catch (e) {
        console.error('Failed to save favorites to storage:', e);
        // Consider showing a toast notification to the user
    }
});
Suggested change
this.saveFavoritesToStorage(favorites);
try {
this.saveFavoritesToStorage(favorites);
} catch (e) {
console.error('Failed to save favorites to storage:', e);
// Optionally, show a toast notification to the user here
}

Copilot uses AI. Check for mistakes.
Comment on lines +193 to +194
} catch {
throw new Error('Failed to parse gist content');
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The empty catch block silently swallows parsing errors. If the gist content is malformed JSON, users won't know why the restore failed. The error message "Failed to parse gist content" should be thrown or logged with more details:

try {
    const data = JSON.parse(file.content);
    return {
        items: data.items || [],
        favorites: data.favorites || {},
        timestamp: data.timestamp || null
    };
} catch (e) {
    console.error('Failed to parse gist content:', e);
    throw new Error(`Failed to parse gist content: ${e.message}`);
}
Suggested change
} catch {
throw new Error('Failed to parse gist content');
} catch (e) {
console.error('Failed to parse gist content:', e);
throw new Error(`Failed to parse gist content: ${e.message}`);

Copilot uses AI. Check for mistakes.
Comment on lines +219 to +225
export function saveGistToken(token) {
try {
localStorage.setItem('gistToken', token);
} catch (e) {
console.error('Failed to save gist token:', e);
}
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Security concern: The GitHub Personal Access Token is stored in localStorage in plain text. While this is standard practice for client-side apps, the PR description should document this security consideration. Users should be aware that tokens stored in localStorage:

  1. Can be accessed by any JavaScript running on the same origin
  2. Are vulnerable to XSS attacks
  3. Should use fine-grained tokens with minimal permissions (only 'gist' scope)

Consider adding a warning message in the modal UI about token security and recommending users create fine-grained tokens limited to gist access only.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +103
const handleSync = async () => {
const savedToken = getGistToken();
if (!savedToken) {
toast.error('Not connected to GitHub');
return;
}

setIsSyncing(true);
try {
const gistSync = new GistSync(savedToken);
await gistSync.syncToGist(items, favorites);
const timestamp = new Date().toISOString();
saveLastSyncTimestamp(timestamp);
setLastSync(timestamp);
toast.success('Synced to GitHub Gist', {
description: 'Your data has been saved successfully'
});
} catch (error) {
toast.error('Sync failed', {
description: error.message
});
} finally {
setIsSyncing(false);
}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Race condition potential: If the user rapidly clicks "Sync to Gist" multiple times, multiple API calls could be in flight simultaneously, potentially creating multiple gists or causing conflicts. While the isSyncing state prevents the button from being clicked again, a more robust solution would be to cancel in-flight requests or queue them. Consider using AbortController for cancellation or a request queue.

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +263
<button
type="button"
onClick={() => setShowToken(!showToken)}
className={`absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded ${isToggled ? 'text-gray-400 hover:text-gray-300' : 'text-gray-400 hover:text-gray-600'}`}
aria-label={showToken ? 'Hide token' : 'Show token'}
>
{showToken ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.05 6.05m3.828 3.828L6.05 6.05m7.875 7.875l3.828 3.828M6.05 6.05L3 3m3.05 3.05l4.243 4.243" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
)}
</button>
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Accessibility issue: The password visibility toggle button only has an aria-label that changes between "Hide token" and "Show token", but screen reader users won't be notified when the visibility state changes. Consider adding aria-pressed or aria-expanded attributes to communicate the current state:

<button
    type="button"
    onClick={() => setShowToken(!showToken)}
    className={...}
    aria-label={showToken ? 'Hide token' : 'Show token'}
    aria-pressed={showToken}
>

Copilot uses AI. Check for mistakes.
isToggled
? 'bg-gray-800/50 hover:bg-gray-700/50 text-gray-200'
: 'bg-white hover:bg-gray-50 text-gray-700'
} ${this.state.isGistConnected ? 'text-green-500' : ''}`}
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The conditional color application ${this.state.isGistConnected ? 'text-green-500' : ''} will override the dark/light mode colors defined earlier in the className. When connected, the button will always be green regardless of the theme. Consider using conditional classes that respect the theme, such as:

${this.state.isGistConnected ? (isToggled ? 'text-green-400' : 'text-green-500') : ''}
Suggested change
} ${this.state.isGistConnected ? 'text-green-500' : ''}`}
}${this.state.isGistConnected ? (isToggled ? ' text-green-400' : ' text-green-500') : ''}`}

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +157
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
});
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

The formatLastSync function has an edge case bug. When the date difference is exactly 7 days or more, it falls through to toLocaleDateString, but the year condition uses !== which could cause unexpected behavior during year transitions. For example, if now is January 1, 2024 and date is December 25, 2023, the year will be shown even though it's recent. Consider using a days threshold instead:

if (diffDays < 7) return `${diffDays}d ago`;
if (diffDays < 365) {
    return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
Suggested change
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
});
if (diffDays < 365) {
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +176
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to fetch gists: ${response.status}`);
}

const gists = await response.json();
return gists.find(gist =>
gist.description === GIST_DESCRIPTION &&
gist.files &&
gist.files[GIST_FILENAME]
) || null;
}

/**
* Create a new gist with the app data
* @param {Array} items - History items
* @param {Object} favorites - Favorites object
* @returns {Promise<Object>} - Created gist object
*/
async createGist(items, favorites) {
const data = this.serializeData(items, favorites);

const response = await fetch(GIST_API_URL, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify({
description: GIST_DESCRIPTION,
public: false,
files: {
[GIST_FILENAME]: {
content: JSON.stringify(data, null, 2)
}
}
})
});

if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to create gist: ${response.status}`);
}

return response.json();
}

/**
* Update an existing gist with the app data
* @param {string} gistId - The gist ID
* @param {Array} items - History items
* @param {Object} favorites - Favorites object
* @returns {Promise<Object>} - Updated gist object
*/
async updateGist(gistId, items, favorites) {
const data = this.serializeData(items, favorites);

const response = await fetch(`${GIST_API_URL}/${gistId}`, {
method: 'PATCH',
headers: this.getHeaders(),
body: JSON.stringify({
description: GIST_DESCRIPTION,
files: {
[GIST_FILENAME]: {
content: JSON.stringify(data, null, 2)
}
}
})
});

if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to update gist: ${response.status}`);
}

return response.json();
}

/**
* Sync data to GitHub Gist (creates or updates)
* @param {Array} items - History items
* @param {Object} favorites - Favorites object
* @returns {Promise<Object>} - Gist object
*/
async syncToGist(items, favorites) {
const existingGist = await this.findExistingGist();

if (existingGist) {
return this.updateGist(existingGist.id, items, favorites);
}

return this.createGist(items, favorites);
}

/**
* Restore data from GitHub Gist
* @returns {Promise<Object|null>} - Restored data or null if not found
*/
async restoreFromGist() {
const existingGist = await this.findExistingGist();

if (!existingGist) {
return null;
}

// Fetch the full gist to get the content
const response = await fetch(`${GIST_API_URL}/${existingGist.id}`, {
method: 'GET',
headers: this.getHeaders()
});

if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `Failed to fetch gist: ${response.status}`);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Duplicate error handling code. The pattern of checking !response.ok, attempting to parse JSON with a catch, and throwing an error is repeated in lines 67-68, 102-104, 133-135, and 174-176. Consider extracting this into a helper method:

async handleApiError(response, operation) {
    const error = await response.json().catch((e) => {
        console.error('Failed to parse error response:', e);
        return {};
    });
    throw new Error(error.message || `Failed to ${operation}: ${response.status}`);
}

Then use: if (!response.ok) await this.handleApiError(response, 'fetch gists');

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.

gist sync

2 participants