feat: Add persistent type filter for pending contacts

- Add query parameter 'types' to GET /api/contacts/pending endpoint
- Save user's type filter selection to localStorage (pendingContactsTypeFilter)
- Restore filter on page reload (Pending Contacts page)
- Contact Management badge shows count based on saved filter
- Default filter: CLI only (type=1)

Frontend changes:
- Add localStorage functions (save/load/set checkboxes)
- Modify loadPendingContacts() to use types from checkboxes
- Modify loadContactCounts() to use filter from localStorage
- Checkbox changes trigger save to localStorage + API reload

Backend changes:
- Add 'types' query parameter to GET /api/contacts/pending
- Filter pending contacts by type before returning
- Validate types parameter (must be 1-4)

This allows users to customize which contact types they want to see
in pending contacts list, and the selection persists across sessions.
The same filter is used consistently across all pages (Pending Contacts
page and Contact Management page).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-04 11:07:31 +01:00
parent 874e11c92a
commit 009d3254b8
3 changed files with 130 additions and 30 deletions

12
.gitignore vendored
View File

@@ -87,26 +87,16 @@ Thumbs.db
# Miscellaneous
# ============================================
.claude/
technotes/prompt-dms.md
technotes/channels.md
technotes/limity.md
technotes/smart-refresh-feature.md
technotes/
docs/Pomoc w zwiększeniu zasięgu.pdf
PRD.md
.gitignore
CLAUDE_CODE_PROMPT.md
technotes/advert-vs-floodadv.md
technotes/conversation-with-user-Xahgmah.md
technotes/issue_diagnosis_missing_recv.md
technotes/reddit_response.txt
technotes/troubleshooting_guide_for_user.md
technotes/reddit_response_final.txt
docs/Mesh Core Lista Zakupowa (repeater Dachowy).pdf
docs/contact-management-next-step.md
docs/response-to-xahgmah.md
docs/UI-Contact-Management-MVP-v1.md
docs/UI-Contact-Management-MVP-v2.md
docs/TEST-PLAN-Contact-Management-v2.md
technotes/API-Diagnostic-Commands-private.md
docs/github-discussion-*.md
docs/github-response-spaces-in-device-name.md

View File

@@ -1606,6 +1606,12 @@ def get_pending_contacts_api():
"""
Get list of contacts awaiting manual approval.
Query parameters:
types (list[int]): Filter by contact types (optional)
Example: ?types=1&types=2 (CLI and REP only)
Valid values: 1 (CLI), 2 (REP), 3 (ROOM), 4 (SENS)
If not provided, returns all pending contacts
Returns:
JSON with pending contacts list with enriched contact data:
{
@@ -1631,9 +1637,26 @@ def get_pending_contacts_api():
}
"""
try:
# Get type filter from query params
types_param = request.args.getlist('types', type=int)
# Validate types (must be 1-4)
if types_param:
invalid_types = [t for t in types_param if t not in [1, 2, 3, 4]]
if invalid_types:
return jsonify({
'success': False,
'error': f'Invalid types: {invalid_types}. Valid types: 1 (CLI), 2 (REP), 3 (ROOM), 4 (SENS)',
'pending': []
}), 400
success, pending, error = cli.get_pending_contacts()
if success:
# Filter by types if specified
if types_param:
pending = [contact for contact in pending if contact.get('type') in types_param]
return jsonify({
'success': True,
'pending': pending,

View File

@@ -136,8 +136,15 @@ function attachManageEventListeners() {
async function loadContactCounts() {
try {
// Fetch pending count
const pendingResp = await fetch('/api/contacts/pending');
// Get saved type filter from localStorage
const savedTypes = loadPendingTypeFilter();
// Build query string with types parameter
const params = new URLSearchParams();
savedTypes.forEach(type => params.append('types', type));
// Fetch pending count (with type filter)
const pendingResp = await fetch(`/api/contacts/pending?${params.toString()}`);
const pendingData = await pendingResp.json();
const pendingBadge = document.getElementById('pendingBadge');
@@ -378,7 +385,11 @@ async function handleCleanupConfirm() {
function initPendingPage() {
console.log('Initializing Pending page...');
// Load pending contacts
// Load saved type filter and set checkboxes
const savedTypes = loadPendingTypeFilter();
setTypeFilterCheckboxes(savedTypes);
// Load pending contacts (will use filter from checkboxes)
loadPendingContacts();
// Attach event listeners for pending page
@@ -402,12 +413,17 @@ function attachPendingEventListeners() {
});
}
// Type filter checkboxes - filter on change
// Type filter checkboxes - save to localStorage and reload on change
['typeFilterCLI', 'typeFilterREP', 'typeFilterROOM', 'typeFilterSENS'].forEach(id => {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.addEventListener('change', () => {
applyPendingFilters();
// Save selected types to localStorage
const selectedTypes = getSelectedTypes();
savePendingTypeFilter(selectedTypes);
// Reload contacts from API with new filter
loadPendingContacts();
});
}
});
@@ -571,6 +587,82 @@ function updateApprovalUI(enabled) {
}
}
// =============================================================================
// Pending Type Filter (localStorage persistence)
// =============================================================================
/**
* Save pending contacts type filter to localStorage.
* This allows the filter to persist across page reloads and be used
* in different parts of the app (Pending page, Contact Management badge, etc.)
*
* @param {Array<number>} types - Array of contact types to filter (1=CLI, 2=REP, 3=ROOM, 4=SENS)
*/
function savePendingTypeFilter(types) {
try {
localStorage.setItem('pendingContactsTypeFilter', JSON.stringify(types));
console.log('Pending type filter saved:', types);
} catch (e) {
console.error('Failed to save pending type filter to localStorage:', e);
}
}
/**
* Load pending contacts type filter from localStorage.
*
* @returns {Array<number>} Array of contact types (default: [1] for CLI only)
*/
function loadPendingTypeFilter() {
try {
const stored = localStorage.getItem('pendingContactsTypeFilter');
if (stored) {
const types = JSON.parse(stored);
// Validate: must be array of valid types
if (Array.isArray(types) && types.every(t => [1, 2, 3, 4].includes(t))) {
console.log('Pending type filter loaded:', types);
return types;
}
}
} catch (e) {
console.error('Failed to load pending type filter from localStorage:', e);
}
// Default: CLI only (most common use case)
return [1];
}
/**
* Set type filter checkboxes based on types array.
* @param {Array<number>} types - Array of contact types (1=CLI, 2=REP, 3=ROOM, 4=SENS)
*/
function setTypeFilterCheckboxes(types) {
const checkboxes = {
1: document.getElementById('typeFilterCLI'),
2: document.getElementById('typeFilterREP'),
3: document.getElementById('typeFilterROOM'),
4: document.getElementById('typeFilterSENS')
};
// Set checkboxes based on types array
for (const [type, checkbox] of Object.entries(checkboxes)) {
if (checkbox) {
checkbox.checked = types.includes(parseInt(type));
}
}
}
/**
* Get currently selected contact types from checkboxes.
* @returns {Array<number>} Array of selected types
*/
function getSelectedTypes() {
const types = [];
if (document.getElementById('typeFilterCLI')?.checked) types.push(1);
if (document.getElementById('typeFilterREP')?.checked) types.push(2);
if (document.getElementById('typeFilterROOM')?.checked) types.push(3);
if (document.getElementById('typeFilterSENS')?.checked) types.push(4);
return types;
}
// =============================================================================
// Pending Contacts Management
// =============================================================================
@@ -590,7 +682,14 @@ async function loadPendingContacts() {
if (countBadge) countBadge.style.display = 'none';
try {
const response = await fetch('/api/contacts/pending');
// Get selected types from checkboxes
const selectedTypes = getSelectedTypes();
// Build query string with types parameter
const params = new URLSearchParams();
selectedTypes.forEach(type => params.append('types', type));
const response = await fetch(`/api/contacts/pending?${params.toString()}`);
const data = await response.json();
if (loadingEl) loadingEl.style.display = 'none';
@@ -835,20 +934,8 @@ function applyPendingFilters() {
const searchInput = document.getElementById('pendingSearchInput');
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
// Get selected types
const selectedTypes = [];
if (document.getElementById('typeFilterCLI')?.checked) selectedTypes.push('CLI');
if (document.getElementById('typeFilterREP')?.checked) selectedTypes.push('REP');
if (document.getElementById('typeFilterROOM')?.checked) selectedTypes.push('ROOM');
if (document.getElementById('typeFilterSENS')?.checked) selectedTypes.push('SENS');
// Filter contacts
// Apply search filter locally (type filter already applied by API)
filteredPendingContacts = pendingContacts.filter(contact => {
// Type filter
if (selectedTypes.length > 0 && !selectedTypes.includes(contact.type_label)) {
return false;
}
// Search filter (name or public_key_prefix)
if (searchTerm) {
const nameMatch = contact.name.toLowerCase().includes(searchTerm);