cyberhybridhub/lib/repositories/question_source_config_repository.dart
nathanAndrinoid a7c370b743
Some checks failed
Admin portal tests / test (push) Has been cancelled
Fix database connection pooling and UI question filtering
- Replaced single Postgres Connection with Pool across the server to prevent runTx locking
- Added _filterForEnabledSources to client's _fetchUnansweredForPanel to strictly enforce local configuration

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 22:04:06 +00:00

171 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import '../data/remote/question_source_remote_store.dart';
import '../models/question_source_preference.dart';
/// Question source mix persisted via `/v1/me/question-sources`.
class QuestionSourceConfigRepository {
QuestionSourceConfigRepository._();
static final QuestionSourceConfigRepository instance =
QuestionSourceConfigRepository._();
static const List<QuestionSourcePreference> defaultPreferences =
<QuestionSourcePreference>[
QuestionSourcePreference(
id: 'market_history',
label: 'Market question',
description: 'Prospective price-move guessing game',
ratio: 50,
),
QuestionSourcePreference(
id: 'spelling',
label: 'Spelling',
description: 'Spell-check quiz from curated word lists',
ratio: 50,
),
];
final QuestionSourceRemoteStore _remote = QuestionSourceRemoteStore();
final ValueNotifier<List<QuestionSourcePreference>> preferences =
ValueNotifier<List<QuestionSourcePreference>>(
List<QuestionSourcePreference>.from(defaultPreferences),
);
String? _activeUid;
List<QuestionSourcePreference> get current =>
mergeWithDefaults(preferences.value);
Future<void> startSession(String firebaseUid) async {
if (_activeUid == firebaseUid) {
return;
}
_activeUid = firebaseUid;
await loadFromRemote();
}
Future<void> endSession() async {
_activeUid = null;
resetToDefaults();
}
Future<void> loadFromRemote() async {
try {
final List<QuestionSourcePreference>? remote =
await _remote.fetchPreferences();
if (remote != null) {
replaceAll(remote);
}
} catch (_) {
replaceAll(defaultPreferences);
}
}
Future<void> saveAll(List<QuestionSourcePreference> next) async {
final List<QuestionSourcePreference> normalized = mergeWithDefaults(next);
if (_activeUid == null) {
replaceAll(normalized);
return;
}
final List<QuestionSourcePreference> saved =
await _remote.savePreferences(normalized);
replaceAll(saved);
}
void replaceAll(List<QuestionSourcePreference> next) {
preferences.value = mergeWithDefaults(next);
}
/// Keeps saved toggles/weights while picking up new default sources and labels.
static List<QuestionSourcePreference> mergeWithDefaults(
List<QuestionSourcePreference> saved,
) {
final Map<String, QuestionSourcePreference> byId =
<String, QuestionSourcePreference>{
for (final QuestionSourcePreference source in saved) source.id: source,
};
return defaultPreferences
.map((QuestionSourcePreference defaults) {
final QuestionSourcePreference? existing = byId[defaults.id];
if (existing == null) {
return defaults;
}
return existing.copyWith(
label: defaults.label,
description: defaults.description,
comingSoon: defaults.comingSoon,
enabled: defaults.comingSoon ? false : existing.enabled,
);
})
.toList(growable: false);
}
void resetToDefaults() {
replaceAll(defaultPreferences);
}
/// True when spelling is the only enabled, non–coming-soon source.
static bool isSpellingOnlyEnabled(List<QuestionSourcePreference> sources) {
final Iterable<QuestionSourcePreference> enabled = sources.where(
(QuestionSourcePreference s) => s.enabled && !s.comingSoon,
);
return enabled.length == 1 && enabled.first.id == 'spelling';
}
/// True when market history is the only enabled, non–coming-soon source.
static bool isMarketOnlyEnabled(List<QuestionSourcePreference> sources) {
final Iterable<QuestionSourcePreference> enabled = sources.where(
(QuestionSourcePreference s) => s.enabled && !s.comingSoon,
);
return enabled.length == 1 && enabled.first.id == 'market_history';
}
static bool isMarketHistoryEnabled(List<QuestionSourcePreference> sources) {
for (final QuestionSourcePreference source in sources) {
if (source.id == 'market_history') {
return source.enabled && !source.comingSoon;
}
}
return false;
}
static bool isSpellingEnabled(List<QuestionSourcePreference> sources) {
for (final QuestionSourcePreference source in sources) {
if (source.id == 'spelling') {
return source.enabled && !source.comingSoon;
}
}
return false;
}
/// True when spelling is enabled and its effective mix is 100%.
static bool isSpellingFullyWeighted(List<QuestionSourcePreference> sources) {
if (!isSpellingEnabled(sources)) {
return false;
}
return (effectiveMix(sources)['spelling'] ?? 0) >= 100;
}
/// Effective share (0–100) for each enabled source; empty when none enabled.
static Map<String, int> effectiveMix(
List<QuestionSourcePreference> sources,
) {
final Iterable<QuestionSourcePreference> enabled =
sources.where((QuestionSourcePreference s) => s.enabled);
final int totalWeight =
enabled.fold<int>(0, (int sum, QuestionSourcePreference s) => sum + s.ratio);
if (totalWeight <= 0) {
return <String, int>{};
}
return <String, int>{
for (final QuestionSourcePreference source in enabled)
source.id: ((source.ratio / totalWeight) * 100).round(),
};
}
}