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
@@ -112,6 +112,7 @@ class _NetworkMapDeviceEditScreenState
decoration: const InputDecoration(
labelText: 'Name (e.g. Floor 3, Rack U-12)',
),
onChanged: (_) => setLocal(() {}),
),
const SizedBox(height: 12),
DropdownButtonFormField<NetworkLocationKind>(
@@ -135,7 +136,11 @@ class _NetworkMapDeviceEditScreenState
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
// Keep Create disabled until a name is entered, instead of
// silently discarding an empty submit after close (QA-048).
onPressed: nameCtrl.text.trim().isEmpty
? null
: () => Navigator.pop(ctx, true),
child: const Text('Create'),
),
],
@@ -8,6 +8,7 @@ import '../../models/network/network_port.dart';
import '../../providers/network_map/network_devices_provider.dart';
import '../../providers/network_map/network_import_provider.dart';
import '../../providers/network_map/network_sites_provider.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_state_view.dart';
import 'widgets/import_diff_view.dart';
@@ -247,12 +248,11 @@ class NetworkMapImportReviewScreen extends ConsumerWidget {
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'Applied ${createdDeviceIds.length} devices, '
'${createdLinkIds.length} links.',
),
));
showSuccessSnackBar(
context,
'Applied ${createdDeviceIds.length} devices, '
'${createdLinkIds.length} links.',
);
context.go('/network-map');
}
}
@@ -6,6 +6,7 @@ import '../../models/network/network_device.dart';
import '../../providers/network_map/network_devices_provider.dart';
import '../../providers/network_map/network_sites_provider.dart';
import '../../providers/profile_provider.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/app_state_view.dart';
import '../../widgets/responsive_body.dart';
@@ -276,9 +277,7 @@ class _UnassignedSection extends ConsumerWidget {
) async {
final sites = ref.read(networkSitesProvider).valueOrNull ?? const [];
if (sites.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Create a site first.')),
);
showWarningSnackBar(context, 'Create a site first.');
return;
}
String? picked = sites.first.id;
@@ -321,9 +320,7 @@ class _UnassignedSection extends ConsumerWidget {
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Moved ${devices.length} devices.')),
);
showSuccessSnackBar(context, 'Moved ${devices.length} devices.');
}
}
}
@@ -98,40 +98,56 @@ class NetworkMapVlanScreen extends ConsumerWidget {
final descCtrl = TextEditingController();
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('New VLAN'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: idCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'VLAN ID (1-4094)'),
autofocus: true,
builder: (ctx) => StatefulBuilder(
// Keep Create disabled until the input is valid, instead of accepting
// the click and silently discarding out-of-range/empty input after the
// dialog closes (QA-048; matches the QA-029 disabled-until-valid fix).
builder: (ctx, setDialogState) {
final parsedId = int.tryParse(idCtrl.text.trim());
final isValid = parsedId != null &&
parsedId >= 1 &&
parsedId <= 4094 &&
nameCtrl.text.trim().isNotEmpty;
return AlertDialog(
title: const Text('New VLAN'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: idCtrl,
keyboardType: TextInputType.number,
decoration:
const InputDecoration(labelText: 'VLAN ID (1-4094)'),
autofocus: true,
onChanged: (_) => setDialogState(() {}),
),
const SizedBox(height: 8),
TextField(
controller: nameCtrl,
decoration: const InputDecoration(labelText: 'Name'),
onChanged: (_) => setDialogState(() {}),
),
const SizedBox(height: 8),
TextField(
controller: descCtrl,
decoration: const InputDecoration(
labelText: 'Description (optional)',
),
),
],
),
const SizedBox(height: 8),
TextField(
controller: nameCtrl,
decoration: const InputDecoration(labelText: 'Name'),
),
const SizedBox(height: 8),
TextField(
controller: descCtrl,
decoration:
const InputDecoration(labelText: 'Description (optional)'),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Create'),
),
],
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: isValid ? () => Navigator.pop(ctx, true) : null,
child: const Text('Create'),
),
],
);
},
),
);
final vlanId = int.tryParse(idCtrl.text.trim());
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/network/network_link.dart';
import '../../../models/network/network_port.dart';
import '../../../providers/network_map/network_devices_provider.dart';
import '../../../utils/snackbar.dart';
/// Opens an M3 dialog to connect [portId] to a port on another device.
///
@@ -69,12 +70,7 @@ class _LinkEditDialogState extends ConsumerState<_LinkEditDialog> {
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to create link: $e'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
showErrorSnackBar(context, 'Failed to create link: $e');
}
} finally {
if (mounted) setState(() => _saving = false);
@@ -128,7 +128,7 @@ class _TopologyLegendState extends State<TopologyLegend>
opacity: _fade,
child: SizeTransition(
sizeFactor: _fade,
axisAlignment: -1,
alignment: const Alignment(-1.0, -1.0),
child: Padding(
padding: const EdgeInsets.only(top: 6),
child: Material(