QA hardening: security RLS fixes, Flutter 3.47.5 upgrade, UI/validation fixes

Security — enforce write authorization server-side (was UI/RPC-only):
- it_service_requests RLS: block cross-office read/edit + self-approve (QA-015)
- pass_slips RLS: owner can complete but not self-approve (QA-046)
- swap_requests RLS: scope select/update to participants + admin (QA-047)
- storage: tighten it_service_attachments + task_attachments write/delete (QA-027)
- admin_user_management edge function: allow programmers to manage users (QA-016)

Fixes:
- workforce generator "uncovered shifts" false alarms (QA-043/044)
- network-map VLAN + New-location dialog validation, disabled-until-valid (QA-048)
- de-flake time-of-day-dependent dashboard metrics test (QA-045)

Toolchain:
- upgrade to Flutter 3.47.5 / Dart 3.13.4; font_awesome_flutter 11.0.0,
  flutter_quill 11.6.0, pdfrx 2.6.5; clear resulting deprecations (QA-002)

analyze clean; 139 tests pass; web build succeeds. Report + evidence in docs/qa/.

Note: also carries the in-progress Brick model cleanup already present in the
working tree. QA-001 (AI keys public in the build) is deferred by owner decision.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-26 11:52:16 +08:00
parent e49b52949c
commit 3cb980b629
125 changed files with 3420 additions and 2853 deletions
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/announcement_comment.dart';
import '../../models/announcement_comment.model.dart';
import '../../providers/announcements_provider.dart';
import '../../providers/profile_provider.dart';
import '../../utils/app_time.dart';
@@ -49,10 +49,9 @@ class _AnnouncementCommentsSectionState
// Comment was posted; only push notification delivery failed.
_controller.clear();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Comment posted, but notifications may not have been sent.'),
),
showInfoSnackBar(
context,
'Comment posted, but notifications may not have been sent.',
);
}
} catch (e) {
@@ -225,7 +224,7 @@ class _CommentTile extends StatelessWidget {
const SizedBox(width: 8),
Flexible(
child: Text(
_relativeTime(comment.createdAt),
AppTime.relative(comment.createdAt),
style: tt.labelSmall
?.copyWith(color: cs.onSurfaceVariant),
overflow: TextOverflow.ellipsis,
@@ -252,12 +251,3 @@ class _CommentTile extends StatelessWidget {
}
}
String _relativeTime(DateTime dt) {
final now = AppTime.now();
final diff = now.difference(dt);
if (diff.inMinutes < 1) return 'just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
return AppTime.formatDate(dt);
}
@@ -4,7 +4,7 @@ import 'dart:math' as math show min;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/announcement.dart';
import '../../models/announcement.model.dart';
import '../../providers/announcements_provider.dart';
import '../../providers/profile_provider.dart';
import '../../theme/m3_motion.dart';
@@ -381,7 +381,7 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
children: [
Flexible(
child: Text(
_relativeTime(widget.announcement.createdAt),
AppTime.relative(widget.announcement.createdAt),
style: tt.labelSmall
?.copyWith(color: cs.onSurfaceVariant),
),
@@ -578,16 +578,6 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
String _relativeTime(DateTime dt) {
final now = AppTime.now();
final diff = now.difference(dt);
if (diff.inMinutes < 1) return 'Just now';
if (diff.inMinutes < 60) return '${diff.inMinutes}m ago';
if (diff.inHours < 24) return '${diff.inHours}h ago';
if (diff.inDays < 7) return '${diff.inDays}d ago';
return AppTime.formatDate(dt);
}
String _roleLabel(String role) {
const labels = {
'admin': 'Admin',
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/announcement.dart';
import '../../models/announcement.model.dart';
import '../../providers/announcements_provider.dart';
import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
@@ -260,12 +260,9 @@ class _CreateAnnouncementContentState
widget.editing != null ? 'Announcement updated.' : 'Announcement posted.');
} on AnnouncementNotificationException {
if (mounted) {
final messenger = ScaffoldMessenger.of(context);
Navigator.of(context).pop();
messenger.showSnackBar(
const SnackBar(
content: Text('Posted, but some notifications failed to send.'),
),
showInfoSnackBarGlobal(
'Posted, but some notifications failed to send.',
);
}
} catch (e) {