Working arrows

This commit is contained in:
nathanAndrinoid 2026-06-04 20:51:03 +00:00
parent 60b14fb49e
commit 1f83f6c9f7
9 changed files with 189 additions and 41 deletions

View File

@ -74,6 +74,13 @@ git pull
sudo systemctl restart cyberhybridhub-api cyberhybridhub-web sudo systemctl restart cyberhybridhub-api cyberhybridhub-web
``` ```
Quick redeploy (code only, no `pub get`, restarts services):
```bash
git pull
./scripts/deploy-dev.sh
```
## Firebase authorized domains ## Firebase authorized domains
```bash ```bash

View File

@ -1,11 +1,33 @@
/// Base URL for the Cyber Hybrid Hub API (Postgres-backed). import 'package:flutter/foundation.dart' show kIsWeb;
///
/// Override at build/run time: const String _apiBaseUrlFromEnvironment = String.fromEnvironment(
/// flutter run --dart-define=API_BASE_URL=http://localhost:3000
const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL', 'API_BASE_URL',
defaultValue: 'http://localhost:3000', defaultValue: 'http://localhost:3000',
); );
bool _isLocalDevHost(String host) {
final String h = host.toLowerCase();
return h == 'localhost' || h == '127.0.0.1';
}
/// Base URL for the Cyber Hybrid Hub API (Postgres-backed).
///
/// Local: `flutter run --dart-define=API_BASE_URL=http://localhost:3000`
///
/// Production web (https://cyberhybridhub.com): always [Uri.base.origin] so
/// /v1 and /hubs hit HAProxy on :443 — never :3000 over HTTP (mixed content).
String get apiBaseUrl {
if (!kIsWeb) {
return _apiBaseUrlFromEnvironment;
}
final Uri page = Uri.base;
if (_isLocalDevHost(page.host)) {
return _apiBaseUrlFromEnvironment;
}
return page.origin;
}
/// SignalR hub for real-time incoming questions. /// SignalR hub for real-time incoming questions.
String get questionsHubUrl => '$apiBaseUrl/hubs/questions'; String get questionsHubUrl => '$apiBaseUrl/hubs/questions';

View File

@ -126,31 +126,34 @@ class HomeScreen extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8), padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: Column( child: FocusScope(
crossAxisAlignment: CrossAxisAlignment.stretch, autofocus: true,
children: <Widget>[ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
const Spacer(), Row(
IconButton( children: <Widget>[
onPressed: hub.closeQuestionPanel, const Spacer(),
tooltip: 'Close', IconButton(
icon: const Icon(Icons.close), onPressed: hub.closeQuestionPanel,
), tooltip: 'Close',
], icon: const Icon(Icons.close),
), ),
const SizedBox(height: 4), ],
Expanded(
child: SwipeQuestionTile(
key: ValueKey<String>(question.id),
questionId: question.id,
busy: hub.questionActionBusy.value,
onSwipeRight: (num answer) =>
hub.submitCurrentAnswer(answer: answer),
onSwipeLeft: hub.deferCurrentQuestion,
), ),
), const SizedBox(height: 4),
], Expanded(
child: SwipeQuestionTile(
key: ValueKey<String>(question.id),
questionId: question.id,
busy: hub.questionActionBusy.value,
onSwipeRight: (num answer) =>
hub.submitCurrentAnswer(answer: answer),
onSwipeLeft: hub.deferCurrentQuestion,
),
),
],
),
), ),
), ),
), ),

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../guid/guid_glyph_shape.dart'; import '../guid/guid_glyph_shape.dart';
import '../services/questions_hub_service.dart';
import '../theme/app_theme.dart'; import '../theme/app_theme.dart';
const double _holdEdgeButtonWidth = 58; const double _holdEdgeButtonWidth = 58;
@ -487,16 +488,29 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
_stepSliderBy(scrollDy > 0 ? -1 : 1); _stepSliderBy(scrollDy > 0 ? -1 : 1);
} }
/// Web/production fallback when [Focus.onKeyEvent] does not receive arrows. /// True while the question panel is open and this tile should own arrow keys.
bool get _captureArrowKeys =>
mounted &&
!widget.busy &&
!_acting &&
QuestionsHubService.instance.questionPanelOpen.value;
/// Primary path for web/production: does not require widget focus.
bool _handleHardwareKey(KeyEvent event) { bool _handleHardwareKey(KeyEvent event) {
if (!_focusNode.hasFocus) { if (!_captureArrowKeys || !_isArrowKey(event.logicalKey)) {
return false; return false;
} }
return _onKeyEvent(_focusNode, event) == KeyEventResult.handled; return _processArrowKey(event) == KeyEventResult.handled;
} }
static bool _isArrowKey(LogicalKeyboardKey key) =>
key == LogicalKeyboardKey.arrowUp ||
key == LogicalKeyboardKey.arrowDown ||
key == LogicalKeyboardKey.arrowLeft ||
key == LogicalKeyboardKey.arrowRight;
/// Arrow up/down step value; hold left/right to defer / save (edge buttons). /// Arrow up/down step value; hold left/right to defer / save (edge buttons).
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { KeyEventResult _processArrowKey(KeyEvent event) {
if (widget.busy || _acting) { if (widget.busy || _acting) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
@ -630,7 +644,6 @@ class _SwipeQuestionTileState extends State<SwipeQuestionTile>
focusNode: _focusNode, focusNode: _focusNode,
autofocus: true, autofocus: true,
descendantsAreFocusable: false, descendantsAreFocusable: false,
onKeyEvent: _onKeyEvent,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
clipBehavior: Clip.none, clipBehavior: Clip.none,

View File

@ -34,23 +34,36 @@ _load_env_api_url() {
API_BASE_URL="https://cyberhybridhub.com" API_BASE_URL="https://cyberhybridhub.com"
_server_api="$(_load_env_api_url "$ROOT/server/.env")" # hosting.env wins over server/.env for web builds (server may still say :3000).
if [ -n "$_server_api" ]; then
API_BASE_URL="$_server_api"
fi
if [ -f "$ROOT/hosting/hosting.env" ]; then if [ -f "$ROOT/hosting/hosting.env" ]; then
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$ROOT/hosting/hosting.env" source "$ROOT/hosting/hosting.env"
API_BASE_URL="${API_PUBLIC_URL:-$API_BASE_URL}" API_BASE_URL="${API_PUBLIC_URL:-$API_BASE_URL}"
fi fi
_server_api="$(_load_env_api_url "$ROOT/server/.env")"
if [ -n "$_server_api" ] && [ ! -f "$ROOT/hosting/hosting.env" ]; then
API_BASE_URL="$_server_api"
fi
if [ -n "$_CLI_OVERRIDE" ]; then if [ -n "$_CLI_OVERRIDE" ]; then
API_BASE_URL="$_CLI_OVERRIDE" API_BASE_URL="$_CLI_OVERRIDE"
fi fi
if [[ "$API_BASE_URL" == http://* ]] || [[ "$API_BASE_URL" == *:3000* ]]; then
echo "ERROR: Production web must not use HTTP or port 3000 in API_BASE_URL." >&2
echo " Got: $API_BASE_URL" >&2
echo " Set API_PUBLIC_URL=https://cyberhybridhub.com in hosting/hosting.env" >&2
exit 1
fi
echo "Building release web with API_BASE_URL=$API_BASE_URL" echo "Building release web with API_BASE_URL=$API_BASE_URL"
flutter build web --release \ _flutter_pub_flag=()
if [ "${SKIP_PUB_GET:-0}" = "1" ]; then
_flutter_pub_flag=(--no-pub)
echo "SKIP_PUB_GET=1 — not running flutter pub get"
fi
flutter build web --release "${_flutter_pub_flag[@]}" \
--dart-define=API_BASE_URL="$API_BASE_URL" \ --dart-define=API_BASE_URL="$API_BASE_URL" \
--dart-define=AUTH_VERBOSE_LOG=true --dart-define=AUTH_VERBOSE_LOG=true
@ -58,4 +71,8 @@ if [ -x "$ROOT/scripts/copy_flutter_js_source_map.sh" ]; then
"$ROOT/scripts/copy_flutter_js_source_map.sh" "$ROOT/build/web" || true "$ROOT/scripts/copy_flutter_js_source_map.sh" "$ROOT/build/web" || true
fi fi
if [ -x "$ROOT/scripts/stamp-web-cache-bust.sh" ]; then
"$ROOT/scripts/stamp-web-cache-bust.sh"
fi
echo "Done: $ROOT/build/web" echo "Done: $ROOT/build/web"

44
scripts/deploy-dev.sh Executable file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Fast deploy: build release web (no pub get) and restart production services.
# Use after git pull when dependencies are already installed.
#
# ./scripts/deploy-dev.sh
#
# Full deploy with package refresh: ./scripts/deploy-production.sh
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
if [ -f "$ROOT/scripts/flutter-env.sh" ]; then
# shellcheck source=/dev/null
source "$ROOT/scripts/flutter-env.sh"
fi
echo "==> Flutter web (release, no pub get)"
SKIP_PUB_GET=1 "$ROOT/scripts/build-production-web.sh"
if grep -qE 'http://cyberhybridhub\.com:3000|https://cyberhybridhub\.com:3000' \
"$ROOT/build/web/main.dart.js" 2>/dev/null; then
echo "ERROR: build/web still bakes :3000 API URL — fix hosting/hosting.env and rebuild." >&2
exit 1
fi
echo "==> Restart services"
if ! command -v systemctl >/dev/null 2>&1; then
echo "systemctl not found; skip service restart." >&2
exit 0
fi
for unit in cyberhybridhub-api cyberhybridhub-web; do
if ! systemctl list-unit-files --type=service "$unit.service" >/dev/null 2>&1; then
echo "Warning: $unit.service not installed (run ./hosting/install-systemd.sh once)" >&2
exit 1
fi
done
sudo systemctl restart cyberhybridhub-api cyberhybridhub-web
sudo systemctl --no-pager --full status cyberhybridhub-api cyberhybridhub-web || true
echo ""
echo "Dev deploy done. Hard-refresh the browser if the UI looks stale."

29
scripts/stamp-web-cache-bust.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# After flutter build web: bust CDN/browser cache for bootstrap + main.dart.js.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
WEB="$ROOT/build/web"
INDEX="$WEB/index.html"
BOOT="$WEB/flutter_bootstrap.js"
if [ ! -f "$INDEX" ]; then
echo "Missing $INDEX — run flutter build web first." >&2
exit 1
fi
STAMP="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo nogit)-$(date +%Y%m%d%H%M%S)"
echo "Web cache-bust stamp: $STAMP"
# index.html: versioned bootstrap URL (index.html is no-cache at origin).
if grep -q 'flutter_bootstrap.js' "$INDEX"; then
sed -i "s|flutter_bootstrap.js[^\"']*|flutter_bootstrap.js?v=${STAMP}|g" "$INDEX"
fi
# flutter_bootstrap.js: versioned main.dart.js load path in buildConfig.
if [ -f "$BOOT" ]; then
sed -i "s|\"mainJsPath\":\"main.dart.js\"|\"mainJsPath\":\"main.dart.js?v=${STAMP}\"|g" "$BOOT"
fi
printf '%s\n' "$STAMP" >"$WEB/BUILD_STAMP.txt"
echo "Stamped: $INDEX and flutter_bootstrap.js (if present)"

View File

@ -138,7 +138,12 @@ void _applyCacheHeaders(HttpResponse response, String filePath) {
} }
final String lower = filePath.toLowerCase(); final String lower = filePath.toLowerCase();
if (lower.endsWith('.html') || lower.endsWith('.htm')) { final String name = lower.split('/').last;
if (lower.endsWith('.html') ||
lower.endsWith('.htm') ||
name.startsWith('main.dart.js') ||
name.startsWith('flutter_bootstrap.js') ||
name == 'flutter.js') {
response.headers.set( response.headers.set(
'Cache-Control', 'Cache-Control',
'no-cache, must-revalidate', 'no-cache, must-revalidate',

View File

@ -36,6 +36,14 @@
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
<body> <body>
<!-- Clear stale Flutter service workers that cache old main.dart.js after deploys. -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (regs) {
for (const r of regs) { r.unregister(); }
});
}
</script>
<!-- <!--
You can customize the "flutter_bootstrap.js" script. You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader This is useful to provide a custom configuration to the Flutter loader