Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ matrix.service == 'cab-standalone-frontend' && format('VITE_COGNITIVE_TOKEN={0}', secrets.VITE_COGNITIVE_TOKEN) || '' }}
# VITE_* values are baked into the bundle at build time. VITE_POWERGRID_SIMU
# is the same-origin base the frontend posts "apply action" to; without it the
# POST lands on / and nginx answers 405. Blank lines are ignored by the action.
build-args: |
${{ matrix.service == 'cab-standalone-frontend' && format('VITE_COGNITIVE_TOKEN={0}', secrets.VITE_COGNITIVE_TOKEN) || '' }}
${{ matrix.service == 'cab-standalone-frontend' && 'VITE_POWERGRID_SIMU=/powergrid-simu' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max
7 changes: 4 additions & 3 deletions config/dev/cab-standalone/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ HOST_IP=172.17.0.1
# Server : http://192.168.208.61:5000/api/v1/recommendation (LAN)
# Public : https://interactiveagent.passerelle.irt-systemx.fr/api/v1/recommendation
RL_AGENT_API_URL=http://host.docker.internal:5123/api/v1/recommendation
RL_AGENT_API_TOKEN=619e707da0981a48d9982019d996f98e20bdd885eab8b20ae05b4ef0f2e95097

RL_AGENT_API_TOKEN=
# PowerGrid simulator upstream for the frontend's /powergrid-simu/ nginx proxy.
# The frontend keeps VITE_POWERGRID_SIMU=/powergrid-simu (same-origin, no CORS);
# only this upstream changes per environment:
# Local dev : http://host.docker.internal:5122/ (simulator container on the host)
# LAN : http://192.168.208.61:5100/
# (Public/k8s uses nginx-kubernetes.conf via the helm chart, not this variable.)
# Public : https://interactivepowergrid.passerelle.irt-systemx.fr/
# (Public/k8s deploys get this location from deploy-chart/configmap-assistant-platform.yaml,
# not from this variable.)
POWERGRID_SIMU_UPSTREAM=http://host.docker.internal:5122/

# VITE build-time variables (also required in frontend/env/.env.local for local dev)
Expand Down
10 changes: 8 additions & 2 deletions config/dev/cab-standalone/nginx-cors-permissive.conf
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,16 @@ server {

# PowerGrid simulator "apply action" proxy. The upstream is environment-specific
# and injected at container start from $POWERGRID_SIMU_UPSTREAM (see start-webui.sh):
# local -> http://host.docker.internal:5122/ LAN -> http://192.168.208.61:5100/
# local -> http://host.docker.internal:5122/
# LAN -> http://192.168.208.61:5100/
# public -> https://interactivepowergrid.passerelle.irt-systemx.fr/
# No explicit Host header: nginx defaults it to $proxy_host (the authority from
# proxy_pass), which is what an ip:port upstream AND a vhost upstream both need.
# Forwarding $http_host instead would make the passerelle route to the wrong vhost.
location /powergrid-simu/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_pass __POWERGRID_SIMU_UPSTREAM__;
}

Expand Down
13 changes: 13 additions & 0 deletions deploy-chart/configmap-assistant-platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,19 @@ data:
proxy_set_header X-Forwarded-For $remote_addr;
}

# PowerGrid (grid2op) simulator "apply action" proxy. Keeps the browser POST
# same-origin (no CORS) — the frontend is built with
# VITE_POWERGRID_SIMU=/powergrid-simu and posts to /powergrid-simu/api/v1/recommendations.
# Host must be the upstream vhost, not $http_host: the passerelle routes by hostname.
location /powergrid-simu/ {
add_header Cache-Control "no-cache";
proxy_set_header Host interactivepowergrid.passerelle.irt-systemx.fr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_pass https://interactivepowergrid.passerelle.irt-systemx.fr/;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
Expand Down
22 changes: 16 additions & 6 deletions deploy-chart/values.ovh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ cabcontext:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-context
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"

cabevent:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-event
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"

cabhistoric:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-historic
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"

cabrecommendation:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-recommendation
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"
extraEnv:
- name: RL_AGENT_API_URL
value: "https://interactiveagent.passerelle.irt-systemx.fr/api/v1/recommendation"
Expand All @@ -34,10 +34,20 @@ cabcapitalization:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-capitalization
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"

frontend:
image:
repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-standalone-frontend
pullPolicy: Always
tag: "1.3.6"
tag: "1.3.7"
extraEnv:
- name: VITE_POWERGRID_SIMU
value: "/powergrid-simu"
- name: POWERGRID_SIMU_UPSTREAM
value: "https://interactivepowergrid.passerelle.irt-systemx.fr/"
- name: VITE_COGNITIVE_TOKEN
valueFrom:
secretKeyRef:
name: cab-frontend
key: vite-cognitive-token
8 changes: 6 additions & 2 deletions frontend/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@ server {
# Proxy for the PowerGrid (grid2op) simulator — keeps the "apply" POST same-origin
# so the browser never does a cross-origin request (avoids CORS entirely).
# Frontend posts to /powergrid-simu/... with VITE_POWERGRID_SIMU=/powergrid-simu.
# NB: this upstream is a LAN address reachable from cab-standalone; adjust per environment.
# NB: this upstream is a LAN address reachable from cab-standalone; adjust per environment
# (public deployments use https://interactivepowergrid.passerelle.irt-systemx.fr/).
# No explicit Host header: nginx defaults it to $proxy_host, which is correct for an
# ip:port upstream and for a vhost upstream behind the passerelle alike.
location /powergrid-simu/ {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_ssl_server_name on;
proxy_ssl_verify off;
proxy_pass http://192.168.208.61:5100/;
}
}
5 changes: 5 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ import Modal from './components/atoms/Modal.vue'
import Navbar from './components/molecules/Navbar.vue'
import { mode } from './plugins/colorMode'
import { useAppStore } from './stores/app'
import { useAuthStore } from './stores/auth'

mode.value = 'auto'

const appStore = useAppStore()

// A session restored from localStorage has no refresh timer running yet; arm it
// so a reloaded tab keeps renewing its token instead of dying at the next poll.
useAuthStore().scheduleRefresh()
</script>
19 changes: 19 additions & 0 deletions frontend/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ export function login(username: string, password: string) {
)
}

/**
* Exchanges the refresh token for a fresh access token. Client credentials are
* injected by the nginx proxy as a Basic auth header (same as `login`), so only
* the grant and the refresh token travel in the body.
*/
export function refreshToken(refresh_token: string) {
return http.post<LoginResponse>(
'/auth/token',
new URLSearchParams({
refresh_token,
grant_type: 'refresh_token',
clientId: 'opfab-client'
}),
// A dead refresh token is expected: the caller turns it into a session
// expiry, so it must not raise a generic error popup of its own.
{ _silent: true }
)
}

export function checkToken(token: string) {
return http.post<{ active: boolean }>(
'/auth/check_token',
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/api/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import i18n from '@/plugins/i18n'
import { useAppStore } from '@/stores/app'
import { useAuthStore } from '@/stores/auth'
import type { Card, CardEvent } from '@/types/cards'
import { handleSessionExpired } from '@/utils/session'

let controller: AbortController = new AbortController()

Expand All @@ -15,7 +16,8 @@ export async function subscribe(
rangeStart?: string
notification?: 'true' | 'false'
},
handler: (card: CardEvent) => void
handler: (card: CardEvent) => void,
retried = false
) {
const authStore = useAuthStore()
const appStore = useAppStore()
Expand All @@ -35,6 +37,13 @@ export async function subscribe(
signal: controller.signal
}
)
// This request bypasses the axios interceptors, so the token dance lives here
if (response.status === 401) {
if (!retried && (await authStore.refresh())) return subscribe(config, handler, true)
appStore.status.notifications.state = 'OFFLINE'
handleSessionExpired()
return response
}
const reader = response.body!.getReader()
const decoder = new TextDecoder('utf-8')
// eslint-disable-next-line no-constant-condition
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/entities/ATM/CAB/Context.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@
const mapStore = useMapStore()
const appStore = useAppStore()

const contextPID = ref(0)
const faulty = ref(false)

Check warning on line 23 in frontend/src/entities/ATM/CAB/Context.vue

View workflow job for this annotation

GitHub Actions / build-and-test

'faulty' is assigned a value but never used
onBeforeMount(async () => {
locale.value = `en-ATM`
contextPID.value = await servicesStore.getContext('ATM', (context: { data: ContextType }) => {
await servicesStore.getContext('ATM', (context: { data: ContextType }) => {
// New context data: iterate over the airplanes array
// 1- Clear last tick's markers and ROUTE waypoints
mapStore.removeCategoryWaypoint('ROUTE')
Expand Down Expand Up @@ -108,6 +107,6 @@
onUnmounted(() => {
locale.value =
window.navigator.language.split('-')[0] || import.meta.env.VITE_DEFAULT_LOCALE || 'en'
clearInterval(contextPID.value)
servicesStore.stopContext()
})
</script>
7 changes: 3 additions & 4 deletions frontend/src/entities/PowerGrid/CAB/Context.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</template>
<script setup lang="ts">
import { TimerReset } from 'lucide-vue-next'
import { computed, onBeforeMount, onUnmounted, ref } from 'vue'
import { computed, onBeforeMount, onUnmounted } from 'vue'
import { ZoomImg } from 'vue3-zoomer'

import Button from '@/components/atoms/Button.vue'
Expand All @@ -52,7 +52,6 @@ import { useServicesStore } from '@/stores/services'
const servicesStore = useServicesStore()
const appStore = useAppStore()

const contextPID = ref(0)

const context = computed(
() =>
Expand All @@ -61,11 +60,11 @@ const context = computed(
)

onBeforeMount(async () => {
contextPID.value = await servicesStore.getContext('PowerGrid')
await servicesStore.getContext('PowerGrid')
})

onUnmounted(() => {
clearInterval(contextPID.value)
servicesStore.stopContext()
})
</script>
<style lang="scss">
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/entities/Railway/CAB/Context.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</Context>
</template>
<script setup lang="ts">
import { onBeforeMount, onUnmounted, ref, watchEffect } from 'vue'
import { onBeforeMount, onUnmounted, watchEffect } from 'vue'

import Context from '@/components/organisms/CAB/Context.vue'
import Map from '@/components/organisms/Map.vue'
Expand All @@ -29,7 +29,6 @@ const mapStore = useMapStore()
const cardsStore = useCardsStore()
const appStore = useAppStore()

const contextPID = ref(0)

watchEffect(() => {
for (const train of cardsStore.cards('Railway'))
Expand All @@ -51,7 +50,7 @@ watchEffect(() => {
})

onBeforeMount(async () => {
contextPID.value = await servicesStore.getContext('Railway', (context) => {
await servicesStore.getContext('Railway', (context) => {
for (const train of context.data.trains)
mapStore.addContextWaypoint({
lat: train.latitude,
Expand All @@ -66,6 +65,6 @@ onBeforeMount(async () => {
})

onUnmounted(() => {
clearInterval(contextPID.value)
servicesStore.stopContext()
})
</script>
1 change: 1 addition & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"modal.error.ERR_BAD_RESPONSE": "Server response error",
"modal.error.ERR_NETWORK": "Network error",
"modal.error.NO_CONTEXT": "Unable to retrieve recommendations without context",
"modal.error.SESSION_EXPIRED": "Your session has expired, please log in again",
"modal.error.default": "Request to {url} failed with code {code} and message {message}",
"modal.info.SUBSCRIPTION_ACTIVE": "A user is logged in, log them out?",
"recommendations.description": "Description",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"modal.error.ERR_BAD_RESPONSE": "Erreur réponse serveur",
"modal.error.ERR_NETWORK": "Erreur réseau",
"modal.error.NO_CONTEXT": "Impossible de récupérer les recommandations en l'absence de contexte",
"modal.error.SESSION_EXPIRED": "Votre session a expiré, veuillez vous reconnecter",
"modal.error.default": "La requête à {url} a échoué avec le code {code} et le message {message}",
"modal.info.SUBSCRIPTION_ACTIVE": "Un utilisateur est connecté, le déconnecter ?",
"recommendations.description": "Description",
Expand Down
Loading
Loading