Working arrows
This commit is contained in:
parent
60b14fb49e
commit
1f83f6c9f7
@ -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
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
44
scripts/deploy-dev.sh
Executable 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
29
scripts/stamp-web-cache-bust.sh
Executable 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)"
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user