feat: persist Facebook posts in DB for instant page load
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Migration 071: Add cached_posts (JSONB) and posts_cached_at to social_media_config - Service: get_cached_posts() and save_all_posts_to_cache() methods - Route: New POST endpoint to save posts cache, pass cached data to template - Template: Render cached posts+charts instantly on page load from DB, save to DB after "Load all" or "Refresh", remove AJAX auto-load Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d3c925338
commit
63ee509e1e
@ -114,6 +114,13 @@ def social_publisher_list():
|
||||
for fp in fb_profiles:
|
||||
fb_stats[fp.company_id] = fp
|
||||
|
||||
# Load cached FB posts from DB for instant rendering
|
||||
cached_fb_posts = {}
|
||||
for cid in (user_company_ids or []):
|
||||
cached = social_publisher.get_cached_posts(cid)
|
||||
if cached:
|
||||
cached_fb_posts[cid] = cached
|
||||
|
||||
return render_template('admin/social_publisher.html',
|
||||
posts=posts,
|
||||
stats=stats,
|
||||
@ -122,7 +129,8 @@ def social_publisher_list():
|
||||
type_filter=type_filter,
|
||||
company_filter=company_filter,
|
||||
configured_companies=configured_companies,
|
||||
fb_stats=fb_stats)
|
||||
fb_stats=fb_stats,
|
||||
cached_fb_posts=cached_fb_posts)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -393,6 +401,27 @@ def social_publisher_fb_posts(company_id):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route('/social-publisher/fb-posts-cache/<int:company_id>', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.MANAGER)
|
||||
def social_publisher_save_posts_cache(company_id):
|
||||
"""Save all loaded FB posts to DB cache for instant page load (AJAX)."""
|
||||
from services.social_publisher_service import social_publisher
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if not _user_can_access_company(db, company_id):
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień.'}), 403
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
data = request.get_json()
|
||||
posts = data.get('posts', []) if data else []
|
||||
if posts:
|
||||
social_publisher.save_all_posts_to_cache(company_id, posts)
|
||||
return jsonify({'success': True, 'saved': len(posts)})
|
||||
|
||||
|
||||
@bp.route('/social-publisher/fb-post-insights/<int:company_id>/<path:post_id>')
|
||||
@login_required
|
||||
@role_required(SystemRole.MANAGER)
|
||||
|
||||
@ -5424,6 +5424,8 @@ class SocialMediaConfig(Base):
|
||||
is_active = Column(Boolean, default=True)
|
||||
debug_mode = Column(Boolean, default=True)
|
||||
config_data = Column(JSONBType)
|
||||
cached_posts = Column(JSONBType)
|
||||
posts_cached_at = Column(DateTime)
|
||||
updated_by = Column(Integer, ForeignKey('users.id'))
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
8
database/migrations/071_social_config_posts_cache.sql
Normal file
8
database/migrations/071_social_config_posts_cache.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- Add cached Facebook posts storage to social_media_config
|
||||
-- Posts are cached in DB so they render instantly on page load
|
||||
|
||||
ALTER TABLE social_media_config ADD COLUMN IF NOT EXISTS cached_posts JSONB;
|
||||
ALTER TABLE social_media_config ADD COLUMN IF NOT EXISTS posts_cached_at TIMESTAMP;
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL ON TABLE social_media_config TO nordabiz_app;
|
||||
@ -603,11 +603,46 @@ class SocialPublisherService:
|
||||
|
||||
# ---- Facebook Page Posts (read from API) ----
|
||||
|
||||
def get_cached_posts(self, company_id: int) -> Dict:
|
||||
"""Return DB-cached posts for instant page load. No API call."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
config = db.query(SocialMediaConfig).filter_by(
|
||||
company_id=company_id, platform='facebook'
|
||||
).first()
|
||||
if not config or not config.cached_posts:
|
||||
return None
|
||||
return {
|
||||
'posts': config.cached_posts.get('posts', []),
|
||||
'cached_at': config.posts_cached_at,
|
||||
'total_count': config.cached_posts.get('total_count', 0),
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _save_posts_to_db(self, company_id: int, posts: list):
|
||||
"""Save posts to DB cache for instant page load."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
config = db.query(SocialMediaConfig).filter_by(
|
||||
company_id=company_id, platform='facebook'
|
||||
).first()
|
||||
if config:
|
||||
config.cached_posts = {'posts': posts, 'total_count': len(posts)}
|
||||
config.posts_cached_at = datetime.now()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to save posts cache for company {company_id}: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_page_recent_posts(self, company_id: int, limit: int = 10,
|
||||
after: str = None) -> Dict:
|
||||
"""Fetch recent posts from company's Facebook page with engagement metrics.
|
||||
|
||||
Uses in-memory cache with 5-minute TTL (first page only).
|
||||
Saves first page to DB for instant page load.
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@ -617,7 +652,7 @@ class SocialPublisherService:
|
||||
|
||||
page_id = config.page_id
|
||||
|
||||
# Cache only first page (no cursor)
|
||||
# In-memory cache only for first page (no cursor)
|
||||
if not after:
|
||||
cache_key = (company_id, page_id)
|
||||
cached = _posts_cache.get(cache_key)
|
||||
@ -640,7 +675,7 @@ class SocialPublisherService:
|
||||
posts = result['posts']
|
||||
next_cursor = result.get('next_cursor')
|
||||
|
||||
# Update cache for first page only
|
||||
# Update in-memory cache for first page only
|
||||
if not after:
|
||||
cache_key = (company_id, page_id)
|
||||
_posts_cache[cache_key] = {
|
||||
@ -657,6 +692,10 @@ class SocialPublisherService:
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def save_all_posts_to_cache(self, company_id: int, posts: list):
|
||||
"""Public method to save all loaded posts to DB cache."""
|
||||
self._save_posts_to_db(company_id, posts)
|
||||
|
||||
def get_post_insights_detail(self, company_id: int, post_id: str) -> Dict:
|
||||
"""Fetch detailed insights for a specific post (impressions, reach, clicks)."""
|
||||
db = SessionLocal()
|
||||
|
||||
@ -447,10 +447,16 @@
|
||||
{% for company_id_key, fb in fb_stats.items() %}
|
||||
<div class="fb-posts-section" id="fbPostsSection-{{ company_id_key }}">
|
||||
<div class="fb-posts-header">
|
||||
<h3>Ostatnie posty na Facebook</h3>
|
||||
<div style="display: flex; gap: var(--spacing-xs);">
|
||||
<button class="btn btn-secondary btn-small" onclick="loadFbPosts({{ company_id_key }}, this)">Zaladuj posty</button>
|
||||
<button class="btn btn-primary btn-small" onclick="loadAllFbPosts({{ company_id_key }}, this)">Zaladuj wszystkie + wykresy</button>
|
||||
<h3>Posty na Facebook
|
||||
{% if cached_fb_posts.get(company_id_key) %}
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 400;">
|
||||
({{ cached_fb_posts[company_id_key].total_count }} postow, cache z {{ cached_fb_posts[company_id_key].cached_at.strftime('%d.%m %H:%M') }})
|
||||
</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div style="display: flex; gap: var(--spacing-xs); align-items: center;">
|
||||
<button class="btn btn-secondary btn-small" onclick="loadFbPosts({{ company_id_key }}, this)">Odswiez posty</button>
|
||||
<button class="btn btn-primary btn-small" onclick="loadAllFbPosts({{ company_id_key }}, this)">Wszystkie + wykresy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fbChartsSection-{{ company_id_key }}" style="display:none;">
|
||||
@ -817,6 +823,7 @@
|
||||
return;
|
||||
}
|
||||
renderFbPosts(companyId, data.posts, data.next_cursor, isAppend);
|
||||
if (!isAppend) saveFbPostsToCache(companyId, data.posts);
|
||||
})
|
||||
.catch(function(err) {
|
||||
btn.textContent = origText;
|
||||
@ -919,6 +926,7 @@
|
||||
container.insertAdjacentHTML('beforeend',
|
||||
'<div style="text-align:center;margin-top:var(--spacing-md);color:var(--text-secondary);font-size:var(--font-size-sm);">Zaladowano wszystkie posty (' + allPosts.length + ')</div>');
|
||||
renderFbCharts(companyId, allPosts);
|
||||
saveFbPostsToCache(companyId, allPosts);
|
||||
} catch (err) {
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
@ -1215,14 +1223,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-load first page of posts on page load
|
||||
function saveFbPostsToCache(companyId, posts) {
|
||||
fetch('/admin/social-publisher/fb-posts-cache/' + companyId, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''},
|
||||
body: JSON.stringify({posts: posts})
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
// Render cached posts from DB on page load (instant, no API call)
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
{% if fb_stats %}
|
||||
{% for company_id_key, fb in fb_stats.items() %}
|
||||
{% if cached_fb_posts %}
|
||||
{% for cid, cache_data in cached_fb_posts.items() %}
|
||||
(function() {
|
||||
var companyId = {{ company_id_key }};
|
||||
var btn = document.querySelector('#fbPostsSection-' + companyId + ' .btn-secondary');
|
||||
if (btn) loadFbPosts(companyId, btn);
|
||||
var companyId = {{ cid }};
|
||||
var cachedPosts = {{ cache_data.posts | tojson }};
|
||||
if (cachedPosts && cachedPosts.length > 0) {
|
||||
renderFbPosts(companyId, cachedPosts, null, false);
|
||||
renderFbCharts(companyId, cachedPosts);
|
||||
}
|
||||
})();
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user