This commit is contained in:
@@ -1541,3 +1541,11 @@ Document the sql schema, include an ERD
|
|||||||
|
|
||||||
### Assistant outcome
|
### Assistant outcome
|
||||||
Added `DATABASE.md` documenting the current SQLite schema at migration version 17, including tables, columns, constraints, indexes, migration history, and a Mermaid ERD. Linked it from the README and verified the focused database tests with `PYTHONPATH=. pytest backend/tests/test_database.py -q` (1 passed).
|
Added `DATABASE.md` documenting the current SQLite schema at migration version 17, including tables, columns, constraints, indexes, migration history, and a Mermaid ERD. Linked it from the README and verified the focused database tests with `PYTHONPATH=. pytest backend/tests/test_database.py -q` (1 passed).
|
||||||
|
|
||||||
|
### User
|
||||||
|
In the toolbar add a search functionality, which filters the log entries on keywords.
|
||||||
|
|
||||||
|
use basic symbols and operators such as with google, and only 'site:' as advanced search operator
|
||||||
|
|
||||||
|
### Assistant outcome
|
||||||
|
Added toolbar search filtering for link title, URL, comment, username, and tags. The query supports implicit AND terms, quoted phrases, `OR`, `-` exclusions, and the `site:` host operator; other advanced operators are treated as ordinary text. Search preferences persist with the existing feed cookie and the rendered-feed regression assertions cover the implementation. JavaScript syntax validation passed; the API suite had 35 passing tests and one unrelated existing Mastodon timestamp assertion expecting text without `UTC`.
|
||||||
|
|||||||
@@ -182,6 +182,7 @@
|
|||||||
## 2026-09-05
|
## 2026-09-05
|
||||||
|
|
||||||
177. Document the sql schema, include an ERD
|
177. Document the sql schema, include an ERD
|
||||||
|
178. In the toolbar add a search functionality, which filters the log entries on keywords. use basic symbols and operators such as with google, and only 'site:' as advanced search operator
|
||||||
177. The popup still shows the sign-in block even though the authenticated session text is displayed; show the block only when signed out.
|
177. The popup still shows the sign-in block even though the authenticated session text is displayed; show the block only when signed out.
|
||||||
178. The authenticated session text and sign-in block are still shown together.
|
178. The authenticated session text and sign-in block are still shown together.
|
||||||
179. Use the VIBE directory to log interactions.
|
179. Use the VIBE directory to log interactions.
|
||||||
|
|||||||
@@ -786,6 +786,7 @@ def test_public_and_admin_pages_render_html():
|
|||||||
assert 'id="auth-profile-link" class="hidden"' in root_page.text
|
assert 'id="auth-profile-link" class="hidden"' in root_page.text
|
||||||
assert 'id="auth-admin-link" class="hidden"' in root_page.text
|
assert 'id="auth-admin-link" class="hidden"' in root_page.text
|
||||||
assert '<select id="tag-filter">' in root_page.text
|
assert '<select id="tag-filter">' in root_page.text
|
||||||
|
assert '<input id="search-input" type="search"' in root_page.text
|
||||||
assert root_page.text.index('class="site-logo"') < root_page.text.index('<section class="toolbar">')
|
assert root_page.text.index('class="site-logo"') < root_page.text.index('<section class="toolbar">')
|
||||||
assert 'logout.js?v=3' in root_page.text
|
assert 'logout.js?v=3' in root_page.text
|
||||||
assert client.get('/alice').status_code == 200
|
assert client.get('/alice').status_code == 200
|
||||||
@@ -829,6 +830,11 @@ def test_public_and_admin_pages_render_html():
|
|||||||
assert 'class="panel-toggle-btn"' in profile_page
|
assert 'class="panel-toggle-btn"' in profile_page
|
||||||
assert 'profile.js?v=6' in profile_page
|
assert 'profile.js?v=6' in profile_page
|
||||||
feed_script = client.get('/static/feed.js?v=7').text
|
feed_script = client.get('/static/feed.js?v=7').text
|
||||||
|
assert 'function parseSearchQuery(value)' in feed_script
|
||||||
|
assert 'site:(\\S+)' in feed_script
|
||||||
|
assert 'token.toUpperCase() === \'OR\'' in feed_script
|
||||||
|
assert 'token.startsWith(\'-\')' in feed_script
|
||||||
|
assert 'matchesSearch(item, pref.search || \'\')' in feed_script
|
||||||
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
||||||
assert 'deleteEntry(item, deleteButton)' in feed_script
|
assert 'deleteEntry(item, deleteButton)' in feed_script
|
||||||
assert 'postToMastodon(item, mastodonButton)' in feed_script
|
assert 'postToMastodon(item, mastodonButton)' in feed_script
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const feedEl = document.getElementById('feed');
|
|||||||
const sortSelect = document.getElementById('sort-select');
|
const sortSelect = document.getElementById('sort-select');
|
||||||
const userFilter = document.getElementById('user-filter');
|
const userFilter = document.getElementById('user-filter');
|
||||||
const tagFilter = document.getElementById('tag-filter');
|
const tagFilter = document.getElementById('tag-filter');
|
||||||
|
const searchInput = document.getElementById('search-input');
|
||||||
|
|
||||||
const cookieName = 'linklog-feed-preferences';
|
const cookieName = 'linklog-feed-preferences';
|
||||||
const accessToken = localStorage.getItem('linklogAccessToken');
|
const accessToken = localStorage.getItem('linklogAccessToken');
|
||||||
@@ -51,6 +52,70 @@ function writePreferences(pref) {
|
|||||||
document.cookie = `${cookieName}=${value}; path=/; max-age=31536000`;
|
document.cookie = `${cookieName}=${value}; path=/; max-age=31536000`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tokenizeSearch(value) {
|
||||||
|
const tokens = [];
|
||||||
|
const pattern = /"([^"]+)"|(\S+)/g;
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(value)) !== null) {
|
||||||
|
tokens.push(match[1] || match[2]);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSearchQuery(value) {
|
||||||
|
const groups = [[]];
|
||||||
|
const excluded = [];
|
||||||
|
const sites = [];
|
||||||
|
|
||||||
|
tokenizeSearch(value).forEach((token) => {
|
||||||
|
if (token.toUpperCase() === 'OR') {
|
||||||
|
groups.push([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isExcluded = token.startsWith('-') && token.length > 1;
|
||||||
|
const term = isExcluded ? token.slice(1) : token;
|
||||||
|
const siteMatch = term.match(/^site:(\S+)$/i);
|
||||||
|
if (siteMatch && !isExcluded) {
|
||||||
|
sites.push(siteMatch[1].toLowerCase());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isExcluded) {
|
||||||
|
excluded.push(term.toLowerCase());
|
||||||
|
} else {
|
||||||
|
groups[groups.length - 1].push(term.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {groups: groups.filter((group) => group.length), excluded, sites};
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchableText(item) {
|
||||||
|
return [
|
||||||
|
item.title,
|
||||||
|
item.url,
|
||||||
|
item.comment,
|
||||||
|
item.user?.username,
|
||||||
|
...(item.tags || []),
|
||||||
|
].filter(Boolean).join(' ').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesSearch(item, query) {
|
||||||
|
if (!query.trim()) return true;
|
||||||
|
const parsed = parseSearchQuery(query);
|
||||||
|
const text = searchableText(item);
|
||||||
|
const matchesSite = parsed.sites.every((site) => {
|
||||||
|
try {
|
||||||
|
const hostname = new URL(item.url).hostname.toLowerCase();
|
||||||
|
return hostname === site || hostname.endsWith(`.${site}`);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const matchesGroup = !parsed.groups.length || parsed.groups.some((group) => group.every((term) => text.includes(term)));
|
||||||
|
const excludesTerm = parsed.excluded.some((term) => text.includes(term));
|
||||||
|
return matchesSite && matchesGroup && !excludesTerm;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return value || 'updated recently';
|
if (Number.isNaN(date.getTime())) return value || 'updated recently';
|
||||||
@@ -278,6 +343,8 @@ async function loadFeed(selectedTag = null) {
|
|||||||
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === activeTag.toLowerCase()));
|
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === activeTag.toLowerCase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items = items.filter((item) => matchesSearch(item, pref.search || ''));
|
||||||
|
|
||||||
if (pref.sort === 'oldest') {
|
if (pref.sort === 'oldest') {
|
||||||
items = [...items].reverse();
|
items = [...items].reverse();
|
||||||
}
|
}
|
||||||
@@ -290,6 +357,7 @@ function syncPreferences() {
|
|||||||
sortSelect.value = pref.sort;
|
sortSelect.value = pref.sort;
|
||||||
userFilter.value = pref.user;
|
userFilter.value = pref.user;
|
||||||
tagFilter.value = pref.tag || '';
|
tagFilter.value = pref.tag || '';
|
||||||
|
searchInput.value = pref.search || '';
|
||||||
|
|
||||||
sortSelect.addEventListener('change', (event) => {
|
sortSelect.addEventListener('change', (event) => {
|
||||||
const next = { ...readPreferences(), sort: event.target.value };
|
const next = { ...readPreferences(), sort: event.target.value };
|
||||||
@@ -309,6 +377,12 @@ function syncPreferences() {
|
|||||||
writePreferences(next);
|
writePreferences(next);
|
||||||
loadFeed(event.target.value);
|
loadFeed(event.target.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', (event) => {
|
||||||
|
const next = { ...readPreferences(), search: event.target.value };
|
||||||
|
writePreferences(next);
|
||||||
|
loadFeed();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPreferences();
|
syncPreferences();
|
||||||
|
|||||||
@@ -221,11 +221,20 @@ body::selection {
|
|||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-tools .toolbar .search-control {
|
||||||
|
flex-basis: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-tools .toolbar select {
|
.header-tools .toolbar select {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-tools .toolbar input {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<section class="toolbar">
|
<section class="toolbar">
|
||||||
|
<label class="search-control">
|
||||||
|
Search
|
||||||
|
<input id="search-input" type="search" placeholder="Search links" autocomplete="off" aria-label="Search links" />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Sort
|
Sort
|
||||||
<select id="sort-select">
|
<select id="sort-select">
|
||||||
@@ -86,6 +90,6 @@
|
|||||||
<script src="/static/auth-header.js?v=3"></script>
|
<script src="/static/auth-header.js?v=3"></script>
|
||||||
<script src="/static/logout.js?v=3"></script>
|
<script src="/static/logout.js?v=3"></script>
|
||||||
<script src="/static/theme.js?v=1"></script>
|
<script src="/static/theme.js?v=1"></script>
|
||||||
<script src="/static/feed.js?v=12"></script>
|
<script src="/static/feed.js?v=13"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user