Merge pull request 'chore: merge develop → main (maintenance window 2026-06-28)' (#7) from develop into main
chore: merge develop → main (maintenance window 2026-06-28)
This commit was merged in pull request #7.
This commit is contained in:
@@ -130,18 +130,6 @@ jobs:
|
|||||||
docker compose pull hiveiq-aria && \
|
docker compose pull hiveiq-aria && \
|
||||||
REGISTRY_URL=${{ secrets.DEV_REGISTRY_URL }} ARIA_VERSION=dev \
|
REGISTRY_URL=${{ secrets.DEV_REGISTRY_URL }} ARIA_VERSION=dev \
|
||||||
docker compose up -d hiveiq-aria"
|
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
|
- name: Cleanup
|
||||||
if: always()
|
if: always()
|
||||||
run: rm -f ~/.ssh/deploy_key
|
run: rm -f ~/.ssh/deploy_key
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: production
|
||||||
env:
|
env:
|
||||||
GIT_SSL_NO_VERIFY: "true"
|
GIT_SSL_NO_VERIFY: "true"
|
||||||
MAVEN_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
|
MAVEN_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
|
||||||
@@ -16,20 +17,12 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify toolchain
|
||||||
|
run: java -version && mvn -version && trivy --version
|
||||||
|
|
||||||
- name: Clean runner disk
|
- name: Clean runner disk
|
||||||
run: docker system prune -f 2>/dev/null || true
|
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
|
- name: Configure Maven settings
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.m2
|
mkdir -p ~/.m2
|
||||||
@@ -73,9 +66,6 @@ jobs:
|
|||||||
- name: Build JAR
|
- name: Build JAR
|
||||||
run: mvn clean package -DskipTests -B
|
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
|
- name: Trivy vulnerability scan
|
||||||
run: |
|
run: |
|
||||||
trivy fs \
|
trivy fs \
|
||||||
@@ -83,6 +73,7 @@ jobs:
|
|||||||
--severity HIGH,CRITICAL \
|
--severity HIGH,CRITICAL \
|
||||||
--ignore-unfixed \
|
--ignore-unfixed \
|
||||||
--scanners vuln \
|
--scanners vuln \
|
||||||
|
--skip-db-update \
|
||||||
--format table \
|
--format table \
|
||||||
target/
|
target/
|
||||||
|
|
||||||
@@ -98,16 +89,12 @@ jobs:
|
|||||||
-DautoUpdate=false \
|
-DautoUpdate=false \
|
||||||
-DdataDirectory=/tmp/owasp-data \
|
-DdataDirectory=/tmp/owasp-data \
|
||||||
-DsuppressionFile=deployment/owasp-suppressions.xml \
|
-DsuppressionFile=deployment/owasp-suppressions.xml \
|
||||||
|
-DassemblyAnalyzerEnabled=false \
|
||||||
-Dformats=HTML,JSON \
|
-Dformats=HTML,JSON \
|
||||||
-B
|
-B
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
run: |
|
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 }}" | \
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
|
||||||
docker login ${{ secrets.REGISTRY_URL }} \
|
docker login ${{ secrets.REGISTRY_URL }} \
|
||||||
-u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
|
-u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
|
||||||
@@ -117,14 +104,3 @@ jobs:
|
|||||||
-t ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA} .
|
-t ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA} .
|
||||||
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:latest
|
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:latest
|
||||||
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA}
|
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
|
shift
|
||||||
done
|
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..."
|
echo "Building JAR..."
|
||||||
mvn clean package -DskipTests -B
|
mvn clean package -DskipTests -B
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
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
|
cat > /app/dist/config.js <<EOF
|
||||||
window.__APP_CONFIG__ = {
|
window.__APP_CONFIG__ = {
|
||||||
apiUrl: '${VITE_API_URL:-http://localhost:8080}',
|
apiUrl: '${VITE_API_URL:-http://localhost:8017}'
|
||||||
authUrl: '${VITE_AUTH_URL:-http://localhost:8082}'
|
|
||||||
};
|
};
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "config.js: apiUrl=${VITE_API_URL:-http://localhost:8080}"
|
echo "config.js: apiUrl=${VITE_API_URL:-http://localhost:8017}"
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.documentElement.classList.add('dark-mode');
|
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 };
|
userInfo = { name: res.data.name, email: res.data.email, role: res.data.role };
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,16 +122,17 @@
|
|||||||
|
|
||||||
// Auto-refresh
|
// Auto-refresh
|
||||||
const REFRESH_SECONDS = 60;
|
const REFRESH_SECONDS = 60;
|
||||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
|
||||||
let secondsLeft = REFRESH_SECONDS;
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
let refreshing = false;
|
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>;
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
|
||||||
|
|
||||||
function toggleAutoRefresh() {
|
function toggleAutoRefresh() {
|
||||||
autoRefreshEnabled = !autoRefreshEnabled;
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,32 +167,13 @@
|
|||||||
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-controls">
|
<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}
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -570,6 +552,39 @@
|
|||||||
.toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
.toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
|
||||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
.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 {
|
.btn-manual-refresh {
|
||||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
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;
|
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:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
.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 — canonical */
|
||||||
.table-card {
|
.table-card {
|
||||||
|
|||||||
@@ -110,16 +110,17 @@
|
|||||||
|
|
||||||
// Auto-refresh
|
// Auto-refresh
|
||||||
const REFRESH_SECONDS = 60;
|
const REFRESH_SECONDS = 60;
|
||||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
|
||||||
let secondsLeft = REFRESH_SECONDS;
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
let refreshing = false;
|
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>;
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
|
||||||
|
|
||||||
function toggleAutoRefresh() {
|
function toggleAutoRefresh() {
|
||||||
autoRefreshEnabled = !autoRefreshEnabled;
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,32 +163,13 @@
|
|||||||
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-controls">
|
<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}
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -551,6 +533,39 @@
|
|||||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||||
|
|
||||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
.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 {
|
.btn-manual-refresh {
|
||||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
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;
|
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:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
.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 — canonical */
|
||||||
.table-card {
|
.table-card {
|
||||||
|
|||||||
@@ -85,16 +85,17 @@
|
|||||||
|
|
||||||
// Auto-refresh
|
// Auto-refresh
|
||||||
const REFRESH_SECONDS = 60;
|
const REFRESH_SECONDS = 60;
|
||||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
|
||||||
let secondsLeft = REFRESH_SECONDS;
|
let secondsLeft = REFRESH_SECONDS;
|
||||||
let refreshing = false;
|
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>;
|
let tickInterval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
|
||||||
|
|
||||||
function toggleAutoRefresh() {
|
function toggleAutoRefresh() {
|
||||||
autoRefreshEnabled = !autoRefreshEnabled;
|
autoRefreshEnabled = !autoRefreshEnabled;
|
||||||
|
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
|
||||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,32 +129,13 @@
|
|||||||
<p>IOC match events detected across the fleet</p>
|
<p>IOC match events detected across the fleet</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-controls">
|
<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}
|
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -290,6 +272,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
.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 {
|
.btn-manual-refresh {
|
||||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
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;
|
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:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
.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 {
|
.page-body {
|
||||||
display: flex; flex: 1; overflow: hidden; min-height: 0; margin: 0 0 16px;
|
display: flex; flex: 1; overflow: hidden; min-height: 0; margin: 0 0 16px;
|
||||||
|
|||||||
@@ -4,21 +4,20 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
__APP_CONFIG__?: {
|
__APP_CONFIG__?: {
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
authUrl?: string;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE = window.__APP_CONFIG__?.apiUrl || 'http://localhost:8017';
|
const ariaApiUrl = window.__APP_CONFIG__?.apiUrl || 'http://localhost:8017';
|
||||||
const AUTH_BASE = window.__APP_CONFIG__?.authUrl || 'http://localhost:8082';
|
const gatewayUrl = ariaApiUrl.replace(/\/aria$/, '');
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE,
|
baseURL: ariaApiUrl,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authApi = axios.create({
|
export const authApi = axios.create({
|
||||||
baseURL: AUTH_BASE,
|
baseURL: gatewayUrl,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@@ -31,7 +25,7 @@ public class SecurityConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(AbstractHttpConfigurer::disable)
|
||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
@@ -46,29 +40,4 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
return http.build();
|
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