Support subpath IIS deployment as a second install method
The app can run as an IIS Application under an existing site (e.g. https://host/ops/) instead of its own site + port: - frontend: vite base via VITE_BASE_PATH; router history, axios baseURL, and root-absolute asset/route paths resolve through utils/basePath.js withBase() - backend: MOUNT_PATH (env or .env) wraps the app in a WSGI middleware that shifts the prefix into SCRIPT_NAME, so one knob serves API + SPA under the mount - docs: INSTALL-WINDOWS-IIS.md section 7b runbook + troubleshooting rows; DEPLOY-WINDOWS-IIS.md pointer; commented examples in deploy/windows/web.config and .env.example Root deployment unchanged (MOUNT_PATH unset, base '/'). Also folds two stray root-absolute callers into the shared plumbing (MachineForm relationship-types fetch, reports CSV window.open).
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import axios from 'axios'
|
||||
import { withBase } from './../utils/basePath'
|
||||
|
||||
// BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath
|
||||
// mount. Keeps the SPA, its API, and IIS all on the same mount path.
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
baseURL: import.meta.env.BASE_URL + 'api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -31,10 +34,11 @@ api.interceptors.response.use(
|
||||
// Only redirect if user was previously logged in (session expired).
|
||||
// Preserve the destination so login returns the user to this page.
|
||||
if (hadToken) {
|
||||
const loginPath = withBase('/login')
|
||||
const here = window.location.pathname + window.location.search
|
||||
const target = here && here !== '/login'
|
||||
? '/login?redirect=' + encodeURIComponent(here)
|
||||
: '/login'
|
||||
const target = here && here !== loginPath
|
||||
? loginPath + '?redirect=' + encodeURIComponent(here)
|
||||
: loginPath
|
||||
window.location.href = target
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// install still renders; each site uploads its own blueprint in Settings.
|
||||
import { reactive } from 'vue'
|
||||
import { settingsApi } from '../api'
|
||||
import { withBase } from '../utils/basePath'
|
||||
|
||||
// Fallback defaults - match the seeded map_blueprint_* setting defaults.
|
||||
const DEFAULTS = {
|
||||
@@ -59,7 +60,7 @@ export function reloadMapConfig() {
|
||||
|
||||
// Blueprint image URL for the given theme ('light' | 'dark').
|
||||
export function blueprintUrlFor(theme) {
|
||||
return theme === 'light' ? state.blueprintLight : state.blueprintDark
|
||||
return withBase(theme === 'light' ? state.blueprintLight : state.blueprintDark)
|
||||
}
|
||||
|
||||
export function useMapConfig() {
|
||||
|
||||
@@ -117,7 +117,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
13
frontend/src/utils/basePath.js
Normal file
13
frontend/src/utils/basePath.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Single source for the app's mount path. Vite injects import.meta.env.BASE_URL
|
||||
// from the build-time `base` (default '/', or e.g. '/ops/' for a subpath IIS
|
||||
// mount). Every root-absolute URL to a Flask-served asset or route must go
|
||||
// through withBase() so it resolves under the mount instead of the server root.
|
||||
export const BASE_URL = import.meta.env.BASE_URL
|
||||
|
||||
// Prefix a root-absolute path (e.g. '/ge-aerospace-logo.svg', '/api', '/tv')
|
||||
// with the mount base. Leaves full URLs (http, data:) untouched.
|
||||
export function withBase(path) {
|
||||
if (!path) return path
|
||||
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
|
||||
return BASE_URL + String(path).replace(/^\//, '')
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// The settings GET is public (jwt optional), so the kiosk dashboard and the
|
||||
// print views can read these without auth. Fetched once and cached per page.
|
||||
import { settingsApi } from '@/api'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
let settingsCache = null
|
||||
|
||||
@@ -38,7 +39,7 @@ export async function getFacilityName() {
|
||||
|
||||
// Main site logo (sidebar, login, dashboard header). Fallback = shipped GE mark.
|
||||
export async function getSiteLogo() {
|
||||
return getSetting('site_logo', '/ge-aerospace-logo.svg')
|
||||
return withBase(await getSetting('site_logo', '/ge-aerospace-logo.svg'))
|
||||
}
|
||||
|
||||
// Logo composited into the center of printer QR codes. Empty = no overlay.
|
||||
@@ -47,17 +48,18 @@ export async function getSiteLogo() {
|
||||
export async function getQrLogo() {
|
||||
const settings = await loadSettings()
|
||||
const value = settings['qr_logo']
|
||||
return (value === undefined || value === null) ? '/ge-monogram.svg' : value
|
||||
return withBase((value === undefined || value === null) ? '/ge-monogram.svg' : value)
|
||||
}
|
||||
|
||||
// Logo printed on machine inspection badges.
|
||||
export async function getBadgeLogo() {
|
||||
return getSetting('badge_logo', '/ge-aerospace-logo.svg')
|
||||
return withBase(await getSetting('badge_logo', '/ge-aerospace-logo.svg'))
|
||||
}
|
||||
|
||||
// Browser-tab favicon. Empty = keep the shipped /favicon.svg.
|
||||
export async function getFavicon() {
|
||||
return getSetting('site_favicon', '')
|
||||
const value = await getSetting('site_favicon', '')
|
||||
return value ? withBase(value) : value
|
||||
}
|
||||
|
||||
// Brand primary color override. Empty = built-in palette from style.css.
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
</template>
|
||||
|
||||
<div class="nav-section">Displays</div>
|
||||
<a href="/shopfloor" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
||||
<a href="/tv" target="_blank" class="external-link">TV Slideshow</a>
|
||||
<a :href="withBase('/shopfloor')" target="_blank" class="external-link">Shopfloor Dashboard</a>
|
||||
<a :href="withBase('/tv')" target="_blank" class="external-link">TV Slideshow</a>
|
||||
|
||||
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link>
|
||||
</nav>
|
||||
@@ -107,6 +107,7 @@ import { useAuthStore } from '../stores/auth'
|
||||
import { currentTheme, toggleTheme } from '../stores/theme'
|
||||
import { dashboardApi, notificationsApi } from '../api'
|
||||
import { getFacilityName, getSiteLogo, getServicenowUrls } from '../utils/siteSettings'
|
||||
import { withBase } from '../utils/basePath'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -118,7 +119,7 @@ const searchQuery = ref('')
|
||||
const navItems = ref([])
|
||||
const activeNotifications = ref([])
|
||||
const facilityName = ref('ShopDB')
|
||||
const siteLogo = ref('/ge-aerospace-logo.svg')
|
||||
const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
|
||||
const servicenowConfig = ref({ enabled: true, searchUrl: '' })
|
||||
|
||||
function getTicketSearchUrl(ticketnumber) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { authApi } from '../api'
|
||||
import { getSiteLogo } from '../utils/siteSettings'
|
||||
import { withBase } from '../utils/basePath'
|
||||
import { apiError } from '../utils/apiError'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -48,7 +49,7 @@ const authStore = useAuthStore()
|
||||
|
||||
const forced = computed(() => authStore.mustChangePassword)
|
||||
|
||||
const siteLogo = ref('/ge-aerospace-logo.svg')
|
||||
const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
@@ -52,6 +52,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { setupApi } from '../api'
|
||||
import { getSiteLogo } from '../utils/siteSettings'
|
||||
import { withBase } from '../utils/basePath'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -67,7 +68,7 @@ function postLoginTarget() {
|
||||
return '/'
|
||||
}
|
||||
|
||||
const siteLogo = ref('/ge-aerospace-logo.svg')
|
||||
const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
|
||||
const mode = ref('login')
|
||||
const username = ref('')
|
||||
const email = ref('')
|
||||
|
||||
@@ -186,10 +186,11 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { notificationsApi, businessUnitsApi, dashboardDefaultsApi } from '@/api'
|
||||
import { getFacilityName, getSiteLogo, getServicenowUrls } from '@/utils/siteSettings'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
const loading = ref(true)
|
||||
const facilityName = ref('ShopDB')
|
||||
const siteLogo = ref('/ge-aerospace-logo.svg')
|
||||
const siteLogo = ref(withBase('/ge-aerospace-logo.svg'))
|
||||
// ServiceNow ticket-link config; loaded on mount. Empty/disabled = plain text.
|
||||
const servicenowConfig = ref({ enabled: true, incidentUrl: '', changeUrl: '' })
|
||||
const businessUnit = ref('')
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="slide"
|
||||
:class="{ active: idx === currentSlide }"
|
||||
>
|
||||
<img :src="basePath + slide.filename" :alt="slide.filename" />
|
||||
<img :src="withBase(basePath + slide.filename)" :alt="slide.filename" />
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
@@ -28,6 +28,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import api from '@/api'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
const INTERVAL = 10 // seconds between slides
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi } from '../../api'
|
||||
import { machinesApi, vendorsApi, locationsApi, modelsApi, businessunitsApi, computersApi, assetsApi, relationshipTypesApi } from '../../api'
|
||||
import ShopFloorMap from '../../components/ShopFloorMap.vue'
|
||||
import Modal from '../../components/Modal.vue'
|
||||
import CustomFieldsInputs from '../../components/CustomFieldsInputs.vue'
|
||||
@@ -463,11 +463,8 @@ onMounted(async () => {
|
||||
|
||||
// Load relationship types separately
|
||||
try {
|
||||
const relRes = await fetch('/api/assets/relationshiptypes')
|
||||
if (relRes.ok) {
|
||||
const relData = await relRes.json()
|
||||
relationshipTypes.value = relData.data || []
|
||||
}
|
||||
const relRes = await relationshipTypesApi.list()
|
||||
relationshipTypes.value = relRes.data.data || []
|
||||
} catch (e) {
|
||||
// Fallback - use hardcoded Controls type
|
||||
relationshipTypes.value = [{ relationshiptypeid: 1, relationshiptype: 'Controls' }]
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { machinesApi } from '../../api'
|
||||
import { getBadgeLogo } from '@/utils/siteSettings'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
import JsBarcode from 'jsbarcode'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -37,7 +38,7 @@ const loading = ref(true)
|
||||
const machine = ref(null)
|
||||
const barcodeEl = ref(null)
|
||||
|
||||
const geLogo = ref('/ge-aerospace-logo.svg')
|
||||
const geLogo = ref(withBase('/ge-aerospace-logo.svg'))
|
||||
|
||||
const isInspection = computed(() => {
|
||||
if (!machine.value) return false
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { reportsApi, businessunitsApi, assetsApi, locationsApi, applicationsApi } from '@/api'
|
||||
import { withBase } from '@/utils/basePath'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -418,7 +419,7 @@ async function runReport(report) {
|
||||
function exportCSV() {
|
||||
if (!currentReport.value) return
|
||||
const params = new URLSearchParams({ format: 'csv', ...filterParams() })
|
||||
window.open(`/api/reports/${currentReport.value.id}?${params}`, '_blank')
|
||||
window.open(withBase(`/api/reports/${currentReport.value.id}?${params}`), '_blank')
|
||||
}
|
||||
|
||||
function clearReport() {
|
||||
|
||||
Reference in New Issue
Block a user