Phase 0: stop-the-bleeding bugfixes

- Defer polling_service.start() and FilterEngine init to a one-shot
  before_request hook so importing app.py no longer spawns a scheduler
  thread (also fixes migrate_*.py import side effects).
- Fix migrate_bookmarks.py: init_db returns None, so use the shared
  `db` instead of assigning its None return.
- Enforce MIN_PASSWORD_LENGTH (8) in the password-reset route for
  consistency with signup (was hardcoded 6).
- post_detail.html: replace undefined moment(...).fromNow() (always
  "Recently") with a new timeago Jinja filter + data-timestamp attrs
  that also drive the existing JS updater; make nl2br escape-then-Markup
  and drop | safe from comment/post content to close the XSS hole.
- filter_pipeline: when AI is disabled but a filterset requires it,
  pass posts through with status=FAILED + explicit error instead of
  silently degrading to no_filter.

No source-file mojibake found; content-encoding ingest is a Phase 3 concern.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-03 01:30:13 -05:00
parent 718cc36973
commit cdba720a1c
4 changed files with 108 additions and 23 deletions

View File

@@ -184,10 +184,16 @@ class FilterEngine:
if self.config.is_ai_enabled():
self._init_stages()
# If AI is disabled but filterset requires it, fall back to no_filter
# If AI is disabled but the filterset requires it, do NOT silently pass
# everything as no_filter. Pass the posts through (so the feed is not
# blanked) but mark every result as FAILED with an explicit error so the
# degradation is observable, not silent.
if not self.config.is_ai_enabled() and filterset_name != 'no_filter':
logger.warning(f"AI disabled but '{filterset_name}' requires AI - falling back to 'no_filter'")
return self._process_no_filter(posts)
logger.warning(
f"AI disabled but filterset '{filterset_name}' requires AI - "
f"passing posts through unfiltered with FAILED status"
)
return self._process_ai_disabled(filterset_name, posts)
# Get pipeline stages for this filterset
stage_names = self._get_stages_for_filterset(filterset_name)
@@ -218,6 +224,28 @@ class FilterEngine:
return results
def _process_ai_disabled(self, filterset_name: str, posts: List[Dict[str, Any]]) -> List[FilterResult]:
"""Pass posts through unfiltered when the requested filterset needs AI
but AI is disabled. Unlike no_filter, every result is marked FAILED with
an explicit error so the degradation is observable rather than silent.
"""
results = []
for post in posts:
result = FilterResult(
post_uuid=post.get('uuid', ''),
passed=True, # do not blank the feed
score=0.5, # neutral score
categories=[],
tags=[],
filterset_name=filterset_name,
processed_at=datetime.now(),
status=ProcessingStatus.FAILED,
error=f"AI disabled: filterset '{filterset_name}' requires AI; passed through unfiltered"
)
results.append(result)
return results
def _get_stages_for_filterset(self, filterset_name: str) -> List[str]:
"""Get pipeline stages to run for a filterset"""
filterset = self.config.get_filterset(filterset_name)