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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user