chore: merge develop → main (maintenance window 2026-06-28) #7
@@ -130,18 +130,6 @@ jobs:
|
||||
docker compose pull hiveiq-aria && \
|
||||
REGISTRY_URL=${{ secrets.DEV_REGISTRY_URL }} ARIA_VERSION=dev \
|
||||
docker compose up -d hiveiq-aria"
|
||||
|
||||
- name: Notify Slack
|
||||
if: always()
|
||||
run: |
|
||||
STATUS="${{ job.status }}"
|
||||
[ "$STATUS" = "success" ] && COLOR="good" || COLOR="danger"
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
RUN_URL="https://hiveiq-gitea.directlx.dev/hiveiq-src/hiveops-aria/actions/runs/${{ github.run_id }}"
|
||||
curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"attachments\":[{\"color\":\"$COLOR\",\"text\":\"*<$RUN_URL|hiveops-aria CD Develop — $STATUS>*\n${{ github.ref_name }} · $SHORT_SHA · ${{ github.actor }}\",\"mrkdwn_in\":[\"text\"],\"footer\":\"Gitea Actions\"}]}"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/deploy_key
|
||||
|
||||
@@ -4,10 +4,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: production
|
||||
env:
|
||||
GIT_SSL_NO_VERIFY: "true"
|
||||
MAVEN_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
|
||||
@@ -16,20 +17,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify toolchain
|
||||
run: java -version && mvn -version && trivy --version
|
||||
|
||||
- name: Clean runner disk
|
||||
run: docker system prune -f 2>/dev/null || true
|
||||
|
||||
- name: Install Java 21 and Maven
|
||||
run: |
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq wget apt-transport-https gnupg
|
||||
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" | tee /etc/apt/sources.list.d/adoptium.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq temurin-21-jdk maven
|
||||
java -version
|
||||
mvn -version
|
||||
|
||||
- name: Configure Maven settings
|
||||
run: |
|
||||
mkdir -p ~/.m2
|
||||
@@ -73,9 +66,6 @@ jobs:
|
||||
- name: Build JAR
|
||||
run: mvn clean package -DskipTests -B
|
||||
|
||||
- name: Install Trivy
|
||||
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
|
||||
- name: Trivy vulnerability scan
|
||||
run: |
|
||||
trivy fs \
|
||||
@@ -83,6 +73,7 @@ jobs:
|
||||
--severity HIGH,CRITICAL \
|
||||
--ignore-unfixed \
|
||||
--scanners vuln \
|
||||
--skip-db-update \
|
||||
--format table \
|
||||
target/
|
||||
|
||||
@@ -98,16 +89,12 @@ jobs:
|
||||
-DautoUpdate=false \
|
||||
-DdataDirectory=/tmp/owasp-data \
|
||||
-DsuppressionFile=deployment/owasp-suppressions.xml \
|
||||
-DassemblyAnalyzerEnabled=false \
|
||||
-Dformats=HTML,JSON \
|
||||
-B
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
if ! command -v docker &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq docker.io
|
||||
fi
|
||||
DOCKER_BUILDKIT=0 docker system prune -f 2>/dev/null || true
|
||||
export DOCKER_BUILDKIT=0
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||
docker login ${{ secrets.REGISTRY_URL }} \
|
||||
-u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
|
||||
@@ -117,14 +104,3 @@ jobs:
|
||||
-t ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA} .
|
||||
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:latest
|
||||
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA}
|
||||
|
||||
- name: Notify Slack
|
||||
if: always()
|
||||
run: |
|
||||
STATUS="${{ job.status }}"
|
||||
[ "$STATUS" = "success" ] && COLOR="good" || COLOR="danger"
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
RUN_URL="https://hiveiq-gitea.directlx.dev/hiveiq-src/hiveops-aria/actions/runs/${{ github.run_id }}"
|
||||
curl -s -X POST "${{ secrets.SLACK_PROD_WEBHOOK_URL }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"attachments\":[{\"color\":\"$COLOR\",\"text\":\"*<$RUN_URL|hiveops-aria CD Production — $STATUS>*\n${{ github.ref_name }} · $SHORT_SHA · ${{ github.actor }}\",\"mrkdwn_in\":[\"text\"],\"footer\":\"Gitea Actions\"}]}"
|
||||
|
||||
@@ -12,7 +12,14 @@ while [[ "$#" -gt 0 ]]; do
|
||||
shift
|
||||
done
|
||||
|
||||
IMAGE="$REGISTRY/hiveiq-aria:latest"
|
||||
# Use :dev tag for the dev registry, :latest for everything else
|
||||
if [[ "$REGISTRY" == *"bcos.dev"* ]]; then
|
||||
TAG="dev"
|
||||
else
|
||||
TAG="latest"
|
||||
fi
|
||||
|
||||
IMAGE="$REGISTRY/hiveiq-aria:$TAG"
|
||||
|
||||
echo "Building JAR..."
|
||||
mvn clean package -DskipTests -B
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Inject runtime config from environment variables into the built SPA.
|
||||
# This file is written to /app/dist/config.js and loaded by index.html
|
||||
# BEFORE the Vite bundle — so window.__APP_CONFIG__ is available at startup.
|
||||
#
|
||||
# Add new config values here and in src/lib/api.ts Window interface.
|
||||
|
||||
cat > /app/dist/config.js <<EOF
|
||||
window.__APP_CONFIG__ = {
|
||||
apiUrl: '${VITE_API_URL:-http://localhost:8080}',
|
||||
authUrl: '${VITE_AUTH_URL:-http://localhost:8082}'
|
||||
apiUrl: '${VITE_API_URL:-http://localhost:8017}'
|
||||
};
|
||||
EOF
|
||||
|
||||
echo "config.js: apiUrl=${VITE_API_URL:-http://localhost:8080}"
|
||||
echo "config.js: apiUrl=${VITE_API_URL:-http://localhost:8017}"
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
onMount(() => {
|
||||
document.documentElement.classList.add('dark-mode');
|
||||
|
||||
authApi.get('/api/auth/users/me').then((res: any) => {
|
||||
authApi.get('/auth/api/users/me').then((res: any) => {
|
||||
userInfo = { name: res.data.name, email: res.data.email, role: res.data.role };
|
||||
}).catch(() => {});
|
||||
});
|
||||
|
||||
@@ -122,16 +122,17 @@
|
||||
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
const _AR_KEY = 'aria_posture_autoRefresh';
|
||||
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
|
||||
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
|
||||
: true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
@@ -166,32 +167,13 @@
|
||||
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
|
||||
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
|
||||
</label>
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -570,6 +552,39 @@
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.auto-refresh-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input { display: none; }
|
||||
.toggle-track {
|
||||
width: 36px; height: 20px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
width: 14px; height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
|
||||
.toggle-label {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
min-width: 110px;
|
||||
}
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
@@ -578,16 +593,6 @@
|
||||
}
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
/* Table card — canonical */
|
||||
.table-card {
|
||||
|
||||
@@ -110,16 +110,17 @@
|
||||
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
const _AR_KEY = 'aria_alerts_autoRefresh';
|
||||
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
|
||||
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
|
||||
: true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
@@ -162,32 +163,13 @@
|
||||
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
|
||||
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
|
||||
</label>
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -551,6 +533,39 @@
|
||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.auto-refresh-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input { display: none; }
|
||||
.toggle-track {
|
||||
width: 36px; height: 20px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
width: 14px; height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
|
||||
.toggle-label {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
min-width: 110px;
|
||||
}
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
@@ -559,16 +574,6 @@
|
||||
}
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
/* Table card — canonical */
|
||||
.table-card {
|
||||
|
||||
@@ -85,16 +85,17 @@
|
||||
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
const _AR_KEY = 'aria_threats_autoRefresh';
|
||||
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
|
||||
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
|
||||
: true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
@@ -128,32 +129,13 @@
|
||||
<p>IOC match events detected across the fleet</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
|
||||
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
|
||||
</label>
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,6 +272,39 @@
|
||||
}
|
||||
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.auto-refresh-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input { display: none; }
|
||||
.toggle-track {
|
||||
width: 36px; height: 20px;
|
||||
background: rgba(255,255,255,0.25);
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
width: 14px; height: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
|
||||
.toggle-label {
|
||||
color: rgba(255,255,255,0.9);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
min-width: 110px;
|
||||
}
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
@@ -298,16 +313,6 @@
|
||||
}
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
.page-body {
|
||||
display: flex; flex: 1; overflow: hidden; min-height: 0; margin: 0 0 16px;
|
||||
|
||||
@@ -4,21 +4,20 @@ declare global {
|
||||
interface Window {
|
||||
__APP_CONFIG__?: {
|
||||
apiUrl?: string;
|
||||
authUrl?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const API_BASE = window.__APP_CONFIG__?.apiUrl || 'http://localhost:8017';
|
||||
const AUTH_BASE = window.__APP_CONFIG__?.authUrl || 'http://localhost:8082';
|
||||
const ariaApiUrl = window.__APP_CONFIG__?.apiUrl || 'http://localhost:8017';
|
||||
const gatewayUrl = ariaApiUrl.replace(/\/aria$/, '');
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE,
|
||||
baseURL: ariaApiUrl,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
export const authApi = axios.create({
|
||||
baseURL: AUTH_BASE,
|
||||
baseURL: gatewayUrl,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
|
||||
@@ -10,12 +10,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -31,7 +25,7 @@ public class SecurityConfig {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.cors(AbstractHttpConfigurer::disable)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
@@ -46,29 +40,4 @@ public class SecurityConfig {
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
String originsEnv = System.getenv("CORS_ALLOWED_ORIGINS");
|
||||
if (originsEnv != null && !originsEnv.isBlank()) {
|
||||
config.setAllowedOrigins(Arrays.asList(originsEnv.split(",")));
|
||||
} else {
|
||||
config.setAllowedOrigins(List.of(
|
||||
"https://aria.bcos.cloud",
|
||||
"https://portal.bcos.cloud",
|
||||
"http://localhost:5181"
|
||||
));
|
||||
}
|
||||
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "X-Service-Secret"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user