Complete LinkSyncServer and LinkSyncExtension implementation

LinkSyncServer:
- Fix app.py imports, add CORS middleware, lifespan events
- Create api/routes.py router aggregator
- Create config/settings.py for centralized configuration
- Rewrite models/base.py with proper relationships and serialization
- Rewrite all API endpoints with real DB integration (auth, links, collections, sync, queries, tags)
- Add admin endpoints (user management, stats, audit log)
- Complete query parser with recursive descent and proper precedence
- Complete query executor with set operations and field filters
- Set up Alembic migrations with initial schema
- Create web interface (templates, CSS, JS)
- Add 42 passing tests (auth, links, collections, queries)
- Add deploy.ps1 and deploy.sh scripts
- Update README with deployment workflow

LinkSyncExtension:
- Create utils/api.js (REST client with retries, auth, error handling)
- Create utils/sync.js (3 sync modes + conflict detection)
- Create utils/collection.js (collection management)
- Create utils/query-engine.js (client-side query parser)
- Rewrite background.js (sync loop, bookmark events, message routing)
- Rewrite popup.js (tabs, settings modal, notifications, CRUD)
- Update popup.html (tabbed interface, query builder, modal)
- Update popup.css (full redesign)
- Create content/content.js (page metadata extraction)
- Create options.html/js (dedicated settings page)
- Generate icons (48x48, 96x96)
- Update manifest.json (host permissions, content scripts, options)
- Create AGENTS.md
This commit is contained in:
DavidSaylor
2026-05-19 13:21:26 -05:00
parent c5d3912070
commit 09d30427f4
54 changed files with 5918 additions and 3177 deletions

View File

@@ -0,0 +1,210 @@
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--secondary: #64748b;
--bg: #f8fafc;
--surface: #ffffff;
--text: #1e293b;
--text-muted: #64748b;
--border: #e2e8f0;
--success: #22c55e;
--error: #ef4444;
--radius: 8px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.navbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand a {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
color: var(--text-muted);
text-decoration: none;
font-weight: 500;
}
.nav-links a:hover {
color: var(--primary);
}
.hero {
text-align: center;
padding: 4rem 1rem;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.hero p {
font-size: 1.125rem;
color: var(--text-muted);
margin-bottom: 2rem;
}
.hero-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-secondary {
background: var(--secondary);
color: white;
}
.btn-secondary:hover {
background: #475569;
}
.section {
margin: 3rem 0;
}
.section h2 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
box-shadow: var(--shadow);
}
.card h3 {
margin-bottom: 0.5rem;
color: var(--primary);
}
.card p {
color: var(--text-muted);
margin-bottom: 1rem;
}
.card a {
color: var(--primary);
text-decoration: none;
}
.feature-list {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 0.75rem;
}
.feature-list li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
.feature-list li::before {
content: "✓";
position: absolute;
left: 0;
color: var(--success);
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: var(--radius);
margin: 1rem 0;
overflow-x: auto;
}
.code-block code {
font-family: "Fira Code", "Cascadia Code", monospace;
font-size: 0.875rem;
}
.footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 4rem;
}
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.nav-links {
flex-wrap: wrap;
justify-content: center;
}
.hero h1 {
font-size: 2rem;
}
.hero-actions {
flex-direction: column;
}
}

View File

@@ -0,0 +1,74 @@
document.addEventListener("DOMContentLoaded", function () {
const apiBase = "/api";
async function apiFetch(endpoint, options = {}) {
const token = localStorage.getItem("token");
const headers = {
"Content-Type": "application/json",
...options.headers,
};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch(`${apiBase}${endpoint}`, {
...options,
headers,
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
window.LinkSync = {
apiFetch,
async getLinks(params = {}) {
const qs = new URLSearchParams(params).toString();
return apiFetch(`/links/?${qs}`);
},
async createLink(data) {
return apiFetch("/links/", {
method: "POST",
body: JSON.stringify(data),
});
},
async updateLink(id, data) {
return apiFetch(`/links/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
},
async deleteLink(id) {
return apiFetch(`/links/${id}`, { method: "DELETE" });
},
async getCollections() {
return apiFetch("/collections/");
},
async createCollection(data) {
return apiFetch("/collections/", {
method: "POST",
body: JSON.stringify(data),
});
},
async executeQuery(expression, limit = 20) {
return apiFetch(`/queries/execute?expression=${encodeURIComponent(expression)}&limit=${limit}`);
},
async login(username, password) {
const formData = new URLSearchParams();
formData.append("username", username);
formData.append("password", password);
const response = await fetch(`${apiBase}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData.toString(),
});
if (!response.ok) throw new Error("Login failed");
const data = await response.json();
localStorage.setItem("token", data.access_token);
return data;
},
logout() {
localStorage.removeItem("token");
},
};
});