Some checks failed
Admin portal tests / test (push) Has been cancelled
- 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>
171 lines
5.3 KiB
Dart
171 lines
5.3 KiB
Dart
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(),
|
||
};
|
||
}
|
||
}
|