Initial commit: Grounded Flutter frontend

A to-do app that doesn't believe you — an enforcement layer rather than a
neutral ledger.

Architecture ported from Autoreceptives/Frontend/Receptive: stacked MVVM with
the mandatory 4-file screen pattern, one ParentViewModel owning the loading /
network / error overlays and the handleError decision tree, one AppDataManager
gateway, dio comms carrying the three identity headers, secure storage with
random-suffixed keys, and a single-chokepoint Navigator. Package root and Dart
package name are both Grounded; org is nya.

The enforcement engine, one unit per formula in utils/:

- DebtEngine      w(class) x severity(d) x decay(t), sublinear severity so old
                  misses cannot swamp the score; abandonment 2x with 30-day
                  decay immunity; late complete retains 30%
- StandingEngine  Good -> Warned -> Grounded -> Lockdown, derived not set;
                  Grounded replaces home with the overdue queue
- CapacityEngine  blocks over-scheduling against p50 of historically completed
                  minutes, with a learned per-category estimation multiplier
- IntegrityEngine session integrity, weekly volume, plyometric contact ceiling
                  and enforced recovery gaps
- ExcuseAnalyser  on-device excuse clustering plus the confrontation copy
- GuardrailEngine distress detection and rationed amnesty
- ToneEngine      all enforcement copy, so the tone cap lives in one place

CommitmentEvent is append-only and is the source of truth rather than the
status field, which is what makes honest history and excuse analysis possible.

Goals contain commitments via parentId, and a task can be run from a
full-screen runner that derives elapsed time from wall-clock so screen-off
cannot lose time. Backgrounding pauses the clock and is counted. The runner is
mirrored into an ongoing notification, with alarm-class full-screen intents
reserved for non-negotiables.

Design language, fonts, icon and native splash are in place; Mason bricks are
retargeted to this project and verified end-to-end.

flutter analyze lib/ reports no errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
This commit is contained in:
alvocool
2026-07-27 09:11:17 +03:00
commit 16bff634b5
315 changed files with 19132 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import '../../about/external/data/ExcuseCluster.dart';
abstract class ConnectExcuseReport {
void onClustersLoaded(List<ExcuseCluster> clusters);
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
import 'ExcuseReportState.dart';
class ExcuseReport extends StatefulWidget {
const ExcuseReport({super.key});
@override
State<ExcuseReport> createState() => ExcuseReportState();
}

View File

@@ -0,0 +1,216 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/internal/application/TextType.dart';
import '../../designs/Component.dart';
import '../../designs/Responsive.dart';
import '../../designs/Shell.dart';
import '../../designs/text/Text.dart';
import '../../utils/Colors.dart';
import '../../utils/CommonUtils.dart';
import 'ConnectExcuseReport.dart';
import 'ExcuseReport.dart';
import 'ViewExcuseReport.dart';
class ExcuseReportState extends State<ExcuseReport>
implements ConnectExcuseReport {
ViewExcuseReport? _model;
List<ExcuseCluster> _clusters = <ExcuseCluster>[];
@override
Widget build(BuildContext context) {
return ViewModelBuilder<ViewExcuseReport>.reactive(
viewModelBuilder: () => ViewExcuseReport(context, this),
onViewModelReady: (viewModel) {
_model = viewModel;
_initiate();
},
builder: (context, viewModel, child) => LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Responsive(
mobile: _mobileView(constraints),
tablet: _mobileView(constraints),
desktop: _mobileView(constraints),
);
},
),
);
}
void _initiate() {
_model?.loadClusters();
}
void _onBack() {
Navigator.pop(context);
}
Widget _mobileView(BoxConstraints constraints) {
final int total = _clusters.fold(0, (sum, item) => sum + item.occurrences);
return Sheet(
eyebrow: "Last 30 days",
title: "Excuses",
onBack: _onBack,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
displayTitle("What you tell\nyourself."),
const SizedBox(height: 14),
text(
"Every deferral you wrote, grouped. Read the concentrations rather than the totals — that is where the pattern is.",
14,
TextType.Regular,
color: colorGrey2,
height: 1.55,
),
const SizedBox(height: 28),
if (_clusters.isEmpty)
emptyState(
CupertinoIcons.text_quote,
"Nothing to confront yet",
"Excuses appear here once you have deferred a few things. There is no shame in an empty page.",
accent: colorPositive,
)
else ...[
card(
background: colorPrimaryDark,
borderColor: colorPrimaryDark,
child: Row(
children: [
Expanded(
child: labelled(
"Total excuses",
"$total",
valueSize: 30,
valueType: TextType.Light,
valueColor: colorWhite,
labelColor: colorWhite.withValues(alpha: 0.45),
),
),
Expanded(
child: labelled(
"Distinct kinds",
"${_clusters.length}",
valueSize: 30,
valueType: TextType.Light,
valueColor: colorWhite,
labelColor: colorWhite.withValues(alpha: 0.45),
),
),
],
),
),
const SizedBox(height: 24),
sectionBreak("The taxonomy", caption: "most frequent first"),
..._clusters.map(_clusterCard),
],
],
),
);
}
Widget _clusterCard(ExcuseCluster cluster) {
final MapEntry<int, int>? peakDay = _peak(cluster.byWeekday);
final MapEntry<int, int>? peakHour = _peak(cluster.byHour);
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: text(cluster.label, 19, TextType.Light,
color: colorPrimaryDark),
),
pill("${cluster.occurrences}×", colorPrimaryDark, colorMuted,
textSize: 10),
],
),
if (cluster.insight.isNotEmpty) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: colorInset,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorBorder, width: 1),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(CupertinoIcons.quote_bubble_fill,
size: 14, color: colorGrey),
const SizedBox(width: 10),
Expanded(
child: text(cluster.insight, 13, TextType.Regular,
color: colorPrimaryDark, height: 1.55),
),
],
),
),
],
hairline(margin: const EdgeInsets.symmetric(vertical: 16)),
Row(
children: [
Expanded(
child: labelled(
"Worst day",
peakDay == null ? "" : weekdayName(peakDay.key),
valueSize: 13,
),
),
Expanded(
child: labelled(
"Worst hour",
peakHour == null ? "" : hourLabel(peakHour.key),
valueSize: 13,
),
),
Expanded(
child: labelled(
"Category",
cluster.dominantCategory.isEmpty
? ""
: cluster.dominantCategory,
valueSize: 13,
),
),
],
),
],
),
),
);
}
MapEntry<int, int>? _peak(Map<int, int> histogram) {
if (histogram.isEmpty) {
return null;
}
MapEntry<int, int>? peak;
for (MapEntry<int, int> entry in histogram.entries) {
if (peak == null || entry.value > peak.value) {
peak = entry;
}
}
return peak;
}
@override
void onClustersLoaded(List<ExcuseCluster> clusters) {
setState(() {
_clusters = clusters;
});
}
}

View File

@@ -0,0 +1,37 @@
import '../../about/external/data/ExcuseCluster.dart';
import '../../about/external/initial/ReportCardRequest.dart';
import '../../utils/ObjectConvertors.dart';
import '../parent/ParentViewModel.dart';
import 'ConnectExcuseReport.dart';
class ViewExcuseReport extends ParentViewModel {
ConnectExcuseReport connection;
ViewExcuseReport(super.context, this.connection);
void loadClusters() async {
if (!await hasNetwork(() => loadClusters())) return;
showLoading("Reading your excuses");
try {
final DateTime now = DateTime.now();
final DateTime start = now.subtract(const Duration(days: 30));
final response =
await getDataManager().getExcuseClusters(ReportCardRequest(
periodStart: start.toIso8601String(),
periodEnd: now.toIso8601String(),
));
final List<ExcuseCluster> clusters =
getExcuseClusterList(response.data);
closeLoading();
connection.onClustersLoaded(clusters);
} catch (e) {
handleError(e, () => loadClusters(), () => dismissError(), "Retry");
}
}
}