{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.auto_feed.page_title') }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-rss"></i>
{{ t('ui.admin.auto_feed.heading') }}
</h2>
<button class="btn btn-fidonet" onclick="showAddFeedModal()">
<i class="fas fa-plus"></i>
{{ t('ui.admin.auto_feed.add_feed') }}
</button>
</div>
<div class="row">
<div class="col-lg-9">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.auto_feed.feed_sources') }}</h6>
</div>
<div class="card-body p-0">
<div id="feedsContainer">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin me-2"></i>
{{ t('ui.admin.auto_feed.loading_feeds') }}
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.auto_feed.statistics') }}</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-8">{{ t('ui.admin.auto_feed.total_feeds') }}:</dt>
<dd class="col-4" id="totalFeeds">-</dd>
<dt class="col-8">{{ t('ui.admin.auto_feed.active_label') }}:</dt>
<dd class="col-4" id="activeFeeds">-</dd>
<dt class="col-8">{{ t('ui.admin.auto_feed.articles_posted') }}:</dt>
<dd class="col-4" id="totalArticles">-</dd>
</dl>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.auto_feed.about_title') }}</h6>
</div>
<div class="card-body">
<p class="small text-muted mb-2">
{{ t('ui.admin.auto_feed.about_text') }}
</p>
<p class="small text-muted mb-0">
{{ t('ui.admin.auto_feed.cron_hint_prefix') }} <code>php scripts/rss_poster.php</code> {{ t('ui.admin.auto_feed.cron_hint_suffix') }}
</p>
</div>
</div>
</div>
</div>
<!-- Add/Edit Feed Modal -->
<div class="modal fade" id="feedModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="feedModalTitle">{{ t('ui.admin.auto_feed.add_feed') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="feedForm">
<input type="hidden" id="feedId">
<div class="mb-3">
<label for="feedName" class="form-label">{{ t('ui.admin.auto_feed.feed_name') }}</label>
<input type="text" class="form-control" id="feedName" name="feed_name" maxlength="100" placeholder="{{ t('ui.admin.auto_feed.feed_name_placeholder') }}">
<div class="form-text">{{ t('ui.admin.auto_feed.feed_name_help') }}</div>
</div>
<div class="mb-3">
<label for="feedUrl" class="form-label">{{ t('ui.admin.auto_feed.feed_url_required') }}</label>
<input type="url" class="form-control" id="feedUrl" name="feed_url" required placeholder="{{ t('ui.admin.auto_feed.feed_url_placeholder', {}, locale, ['common']) }}">
<div class="form-text">{{ t('ui.admin.auto_feed.feed_url_help') }}</div>
</div>
<div class="mb-3">
<label for="feedSourceType" class="form-label">{{ t('ui.admin.auto_feed.source_type') }}</label>
<select class="form-select" id="feedSourceType" name="source_type">
<option value="rss">{{ t('ui.admin.auto_feed.source_type_rss') }}</option>
<option value="bluesky">{{ t('ui.admin.auto_feed.source_type_bluesky') }}</option>
</select>
<div class="form-text">{{ t('ui.admin.auto_feed.source_type_help') }}</div>
</div>
<div class="mb-3">
<label for="feedEchoarea" class="form-label">{{ t('ui.admin.auto_feed.echo_area_required') }}</label>
<select class="form-select" id="feedEchoarea" name="echoarea_id" required>
<option value="">{{ t('ui.admin.auto_feed.loading_echo_areas') }}</option>
</select>
<div class="form-text">{{ t('ui.admin.auto_feed.echo_area_help') }}</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<label for="feedUserSearch" class="form-label">{{ t('ui.admin.auto_feed.post_as_user_required') }}</label>
<input type="hidden" id="feedUser" name="post_as_user_id" required>
<div class="position-relative">
<input type="text" class="form-control" id="feedUserSearch" autocomplete="off" placeholder="{{ t('ui.admin.auto_feed.user_search_placeholder') }}">
<div id="feedUserResults" class="list-group position-absolute w-100 shadow-sm d-none" style="z-index: 1060;"></div>
</div>
<div class="form-text">{{ t('ui.admin.auto_feed.post_as_user_help') }}</div>
</div>
<div class="col-md-4">
<label for="feedMaxArticles" class="form-label">{{ t('ui.admin.auto_feed.max_articles') }}</label>
<input type="number" class="form-control" id="feedMaxArticles" name="max_articles_per_check" value="10" min="1" max="50">
<div class="form-text">{{ t('ui.admin.auto_feed.per_check') }}</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="feedThreadReplies" name="thread_replies">
<label class="form-check-label fw-semibold" for="feedThreadReplies">
{{ t('ui.admin.auto_feed.thread_replies') }}
</label>
<div class="form-text">{{ t('ui.admin.auto_feed.thread_replies_help') }}</div>
</div>
</div>
<div class="col-md-4" id="threadLookupLimitGroup" style="display:none;">
<label for="feedThreadLookupLimit" class="form-label">{{ t('ui.admin.auto_feed.thread_lookup_limit') }}</label>
<input type="number" class="form-control" id="feedThreadLookupLimit" name="thread_lookup_limit" value="1000" min="100" max="10000">
<div class="form-text">{{ t('ui.admin.auto_feed.thread_lookup_limit_help') }}</div>
</div>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="feedActive" name="active" checked>
<label class="form-check-label" for="feedActive">
{{ t('ui.admin.auto_feed.active_label') }}
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel') }}</button>
<button type="button" class="btn btn-fidonet" onclick="saveFeed()">
<i class="fas fa-save"></i>
{{ t('ui.common.save') }}
</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ t('ui.admin.auto_feed.confirm_delete_title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{{ t('ui.admin.auto_feed.confirm_delete_text_prefix') }} <strong id="deleteFeedName"></strong>?</p>
<p class="text-warning"><i class="fas fa-exclamation-triangle"></i> {{ t('ui.admin.auto_feed.confirm_delete_warning') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel') }}</button>
<button type="button" class="btn btn-danger" id="confirmDelete">
<i class="fas fa-trash"></i>
{{ t('ui.common.delete') }}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentFeedId = null;
let allFeeds = [];
let selectedFeedUser = null;
let userSearchTimer = null;
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
$(document).ready(function() {
const initAutoFeedPage = function() {
loadFeeds();
loadStats();
loadEchoareas();
$('#feedUrl').on('input', function() {
if (/^https?:\/\/(www\.)?bsky\.app\/profile\//i.test($(this).val())) {
$('#feedSourceType').val('bluesky');
}
});
$('#feedThreadReplies').on('change', function() {
$('#threadLookupLimitGroup').toggle($(this).is(':checked'));
});
$('#feedUserSearch').on('input', function() {
selectedFeedUser = null;
$('#feedUser').val('');
updateFeedUserValidity();
queueUserSearch($(this).val());
});
$('#feedUserSearch').on('focus', function() {
queueUserSearch($(this).val());
});
$('#feedUserSearch').on('blur', function() {
setTimeout(function() {
$('#feedUserResults').addClass('d-none').empty();
updateFeedUserValidity();
}, 150);
});
};
if (typeof loadUserSettings === 'function') {
loadUserSettings().finally(initAutoFeedPage);
return;
}
initAutoFeedPage();
});
function loadFeeds() {
$('#feedsContainer').html(`<div class="loading-spinner"><i class="fas fa-spinner fa-spin me-2"></i>${uiT('ui.admin.auto_feed.loading_feeds', 'Loading feeds...')}</div>`);
$.get('/admin/api/auto-feed/feeds')
.done(function(data) {
console.log('API response:', data);
if (!data || !data.feeds) {
console.error('Invalid response structure:', data);
$('#feedsContainer').html(`<div class="text-center text-danger py-4">${uiT('ui.admin.auto_feed.invalid_api_response', 'Invalid API response')}</div>`);
return;
}
allFeeds = data.feeds;
displayFeeds(allFeeds);
})
.fail(function(xhr, status, error) {
console.error('API error:', status, error);
$('#feedsContainer').html(`<div class="text-center text-danger py-4">${uiT('ui.admin.auto_feed.failed_to_load_feeds_prefix', 'Failed to load feeds:')} ${escapeHtml(error)}</div>`);
});
}
function displayFeeds(feeds) {
const container = $('#feedsContainer');
let html = '';
if (feeds.length === 0) {
html = `<div class="text-center text-muted py-4">${uiT('ui.admin.auto_feed.no_feeds_configured', 'No feeds configured')}</div>`;
} else {
html = `
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th width="5%"></th>
<th width="18%">${uiT('ui.admin.auto_feed.name_url', 'Name/URL')}</th>
<th width="10%">${uiT('ui.admin.auto_feed.source_type', 'Source Type')}</th>
<th width="18%">${uiT('ui.admin.auto_feed.echo_area', 'Echo Area')}</th>
<th width="14%">${uiT('ui.admin.auto_feed.post_as', 'Post As')}</th>
<th width="10%">${uiT('ui.admin.auto_feed.articles', 'Articles')}</th>
<th width="15%">${uiT('ui.admin.auto_feed.last_check', 'Last Check')}</th>
<th width="10%">${uiT('ui.admin.auto_feed.actions', 'Actions')}</th>
</tr>
</thead>
<tbody>
`;
feeds.forEach(function(feed) {
const isActive = feed.active;
const rowClass = isActive ? '' : 'table-secondary';
const statusIcon = isActive ?
`<i class="fas fa-check-circle text-success" title="${uiT('ui.admin.auto_feed.active_label', 'Active')}"></i>` :
`<i class="fas fa-pause-circle text-warning" title="${uiT('ui.admin.auto_feed.inactive_label', 'Inactive')}"></i>`;
const lastCheck = feed.last_check ?
new Date(feed.last_check).toLocaleString() :
`<span class="text-muted">${uiT('ui.admin.auto_feed.never', 'Never')}</span>`;
const feedName = escapeHtml(feed.feed_name || uiT('ui.admin.auto_feed.unnamed_feed', 'Unnamed Feed'));
const feedUrl = escapeHtml(feed.feed_url);
const truncatedUrl = feedUrl.length > 50 ? feedUrl.substring(0, 47) + '...' : feedUrl;
html += `
<tr class="${rowClass}">
<td class="text-center">${statusIcon}</td>
<td>
<strong>${feedName}</strong><br>
<small class="text-muted" title="${feedUrl}">${truncatedUrl}</small>
${feed.last_error ? `<br><small class="text-danger"><i class="fas fa-exclamation-triangle"></i> ${uiT('ui.common.error', 'Error')}</small>` : ''}
</td>
<td>
<span class="badge bg-secondary">${formatSourceType(feed.source_type || 'rss')}</span>
</td>
<td>
<span class="font-monospace">${escapeHtml(feed.echoarea_tag || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}</span><br>
<small class="text-muted">@ ${escapeHtml(feed.echoarea_domain || uiT('ui.admin.auto_feed.unknown', 'Unknown'))}</small>
</td>
<td>
<small>${escapeHtml(feed.username || (uiT('ui.admin.auto_feed.user_prefix', 'User #') + feed.post_as_user_id))}</small>
</td>
<td>
<span class="badge bg-secondary">${feed.articles_posted || 0}</span>
</td>
<td>
<small>${lastCheck}</small>
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-success" onclick="checkFeed(${feed.id})" title="${uiT('ui.admin.auto_feed.check_now', 'Check now')}">
<i class="fas fa-sync"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="editFeed(${feed.id})" title="${uiT('ui.common.edit', 'Edit')}">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="showDeleteModal(${feed.id}, '${escapeHtml(feedName)}')" title="${uiT('ui.common.delete', 'Delete')}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
}
container.html(html);
}
function showAddFeedModal() {
currentFeedId = null;
$('#feedModalTitle').text(uiT('ui.admin.auto_feed.add_feed', 'Add Feed'));
$('#feedForm')[0].reset();
$('#feedId').val('');
$('#feedActive').prop('checked', true);
$('#feedMaxArticles').val(10);
$('#feedSourceType').val('rss');
$('#feedThreadReplies').prop('checked', false);
$('#feedThreadLookupLimit').val(1000);
$('#threadLookupLimitGroup').hide();
setSelectedFeedUser(null);
$('#feedModal').modal('show');
}
function editFeed(id) {
currentFeedId = id;
$('#feedModalTitle').text(uiT('ui.admin.auto_feed.edit_feed', 'Edit Feed'));
$.get(`/admin/api/auto-feed/feeds/${id}`)
.done(function(data) {
const feed = data.feed;
$('#feedName').val(feed.feed_name || '');
$('#feedUrl').val(feed.feed_url);
$('#feedSourceType').val(feed.source_type || 'rss');
$('#feedEchoarea').val(feed.echoarea_id);
setSelectedFeedUser({
id: feed.post_as_user_id,
username: feed.username || '',
real_name: feed.real_name || ''
});
$('#feedMaxArticles').val(feed.max_articles_per_check || 10);
$('#feedActive').prop('checked', feed.active);
const threadReplies = !!feed.thread_replies;
$('#feedThreadReplies').prop('checked', threadReplies);
$('#feedThreadLookupLimit').val(feed.thread_lookup_limit || 1000);
$('#threadLookupLimitGroup').toggle(threadReplies);
$('#feedModal').modal('show');
})
.fail(function() {
showError(uiT('ui.admin.auto_feed.load_details_failed', 'Failed to load feed details'));
});
}
function saveFeed() {
const form = $('#feedForm');
updateFeedUserValidity();
if (!form[0].checkValidity()) {
form[0].reportValidity();
return;
}
const formData = {
feed_name: $('#feedName').val() || null,
feed_url: $('#feedUrl').val(),
source_type: $('#feedSourceType').val() || 'rss',
echoarea_id: parseInt($('#feedEchoarea').val()),
post_as_user_id: parseInt($('#feedUser').val(), 10),
max_articles_per_check: parseInt($('#feedMaxArticles').val()),
active: $('#feedActive').is(':checked'),
thread_replies: $('#feedThreadReplies').is(':checked'),
thread_lookup_limit: parseInt($('#feedThreadLookupLimit').val()) || 1000
};
const url = currentFeedId ? `/admin/api/auto-feed/feeds/${currentFeedId}` : '/admin/api/auto-feed/feeds';
const method = currentFeedId ? 'PUT' : 'POST';
$.ajax({
url: url,
method: method,
contentType: 'application/json',
data: JSON.stringify(formData),
success: function(response) {
$('#feedModal').modal('hide');
const defaultMessage = currentFeedId
? uiT('ui.admin.auto_feed.updated_success', 'Feed updated successfully')
: uiT('ui.admin.auto_feed.created_success', 'Feed created successfully');
const successMessage = window.getApiMessage
? window.getApiMessage(response, defaultMessage)
: defaultMessage;
showSuccess(successMessage);
loadFeeds();
loadStats();
},
error: function(xhr) {
const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.operation_failed', 'Operation failed'));
showError(error);
}
});
}
function showDeleteModal(id, name) {
currentFeedId = id;
$('#deleteFeedName').text(name);
$('#deleteModal').modal('show');
$('#confirmDelete').off('click').on('click', function() {
deleteFeed();
});
}
function deleteFeed() {
$.ajax({
url: `/admin/api/auto-feed/feeds/${currentFeedId}`,
method: 'DELETE',
success: function(response) {
$('#deleteModal').modal('hide');
const successMessage = window.getApiMessage
? window.getApiMessage(response, uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully'))
: uiT('ui.admin.auto_feed.deleted_success', 'Feed deleted successfully');
showSuccess(successMessage);
loadFeeds();
loadStats();
},
error: function(xhr) {
const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.delete_failed', 'Delete failed'));
showError(error);
}
});
}
function checkFeed(id) {
showInfo(uiT('ui.admin.auto_feed.checking_feed', 'Checking feed...'));
$.post(`/admin/api/auto-feed/check/${id}`)
.done(function(data) {
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted }))
: uiT('ui.admin.auto_feed.checked_articles_posted', 'Feed checked: {count} new article(s) posted', { count: data.articles_posted });
showSuccess(successMessage);
loadFeeds();
loadStats();
})
.fail(function(xhr) {
const error = apiError(xhr.responseJSON, uiT('ui.admin.auto_feed.check_failed', 'Check failed'));
showError(error);
});
}
function loadStats() {
$.get('/admin/api/auto-feed/stats')
.done(function(data) {
$('#totalFeeds').text(data.total_feeds || 0);
$('#activeFeeds').text(data.active_feeds || 0);
$('#totalArticles').text(data.total_articles || 0);
});
}
function loadEchoareas() {
$.get('/api/echoareas')
.done(function(data) {
const select = $('#feedEchoarea');
select.empty();
select.append(`<option value="">${uiT('ui.admin.auto_feed.select_echo_area', 'Select echo area...')}</option>`);
data.echoareas.forEach(function(area) {
const label = `${area.tag} @ ${area.domain}${area.description ? ' - ' + area.description : ''}`;
select.append(`<option value="${area.id}">${escapeHtml(label)}</option>`);
});
});
}
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
if (payload && payload.error) {
return String(payload.error);
}
return fallback;
}
function formatSourceType(sourceType) {
if (sourceType === 'bluesky') {
return uiT('ui.admin.auto_feed.source_type_bluesky', 'Bluesky');
}
return uiT('ui.admin.auto_feed.source_type_rss', 'RSS/Atom');
}
function queueUserSearch(query) {
clearTimeout(userSearchTimer);
userSearchTimer = setTimeout(function() {
searchFeedUsers(query);
}, 200);
}
function searchFeedUsers(query) {
const search = (query || '').trim();
const results = $('#feedUserResults');
if (search.length < 2) {
results.addClass('d-none').empty();
return;
}
results.removeClass('d-none').html(
`<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.searching_users', 'Searching users...')}</div>`
);
$.get('/admin/api/users', { search: search, limit: 10 })
.done(function(data) {
const users = data && data.users ? data.users : [];
displayUserResults(users);
})
.fail(function() {
results.removeClass('d-none').html(
`<div class="list-group-item text-danger">${uiT('ui.admin.auto_feed.user_search_failed', 'User search failed')}</div>`
);
});
}
function displayUserResults(users) {
const results = $('#feedUserResults');
results.empty();
if (!users.length) {
results.removeClass('d-none').html(
`<div class="list-group-item text-muted">${uiT('ui.admin.auto_feed.no_users_found', 'No users found')}</div>`
);
return;
}
users.forEach(function(user) {
const label = formatUserLabel(user);
const item = $(`
<button type="button" class="list-group-item list-group-item-action">
<div class="fw-semibold">${escapeHtml(user.username || '')}</div>
<small class="text-muted">${escapeHtml(user.real_name || uiT('ui.admin.auto_feed.no_real_name', 'No real name'))}</small>
</button>
`);
item.on('mousedown', function(event) {
event.preventDefault();
setSelectedFeedUser(user);
results.addClass('d-none').empty();
});
item.attr('title', label);
results.append(item);
});
results.removeClass('d-none');
}
function setSelectedFeedUser(user) {
selectedFeedUser = user && user.id ? user : null;
if (!selectedFeedUser) {
$('#feedUser').val('');
$('#feedUserSearch').val('');
updateFeedUserValidity();
return;
}
$('#feedUser').val(selectedFeedUser.id);
$('#feedUserSearch').val(formatUserLabel(selectedFeedUser));
updateFeedUserValidity();
}
function updateFeedUserValidity() {
const input = document.getElementById('feedUserSearch');
if (!input) return;
if ($('#feedUser').val()) {
input.setCustomValidity('');
} else {
input.setCustomValidity(uiT('ui.admin.auto_feed.select_valid_user', 'Select a valid user from the search results'));
}
}
function formatUserLabel(user) {
const username = user.username || (uiT('ui.admin.auto_feed.user_prefix', 'User #') + user.id);
const realName = user.real_name || '';
return realName ? `${username} (${realName})` : username;
}
</script>
{% endblock %}
|