diff --git a/frontend/src/utils/siteSettings.js b/frontend/src/utils/siteSettings.js
index a333a950..7edf5914 100644
--- a/frontend/src/utils/siteSettings.js
+++ b/frontend/src/utils/siteSettings.js
@@ -115,6 +115,21 @@ export async function getServicenowUrls() {
return { enabled, incidentUrl, changeUrl, searchUrl }
}
+// Deep links into the Azure portal for a device ShopDB knows the Entra ids of.
+//
+// Both are TEMPLATES held in settings rather than built here, because the host
+// is a tenant fact: this site is on the US Government cloud (portal.azure.us)
+// and the commercial cloud is portal.azure.com. Microsoft also moves these
+// blade paths from time to time, and a site can fix that itself by editing a
+// setting instead of waiting for a release.
+//
+// Empty is the default and means "no link" - the id still renders as text.
+export async function getEntraDeviceUrls() {
+ const entraUrl = await getSetting('entra_device_url', '')
+ const intuneUrl = await getSetting('intune_device_url', '')
+ return { entraUrl, intuneUrl }
+}
+
// Printer hostname template. {ip} is replaced with the dash-separated IP.
export async function getPrinterHostnameTemplate() {
return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net') // ADR-015-OK: GE Aerospace-wide domain, and only the DEFAULT of a documented setting every site can override.
diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue
index 044691dc..820aa606 100644
--- a/plugins/computers/frontend/views/PCDetail.vue
+++ b/plugins/computers/frontend/views/PCDetail.vue
@@ -196,13 +196,35 @@
>
Entra Device Info
+
Device ID
-
{{ computer.computer.azuredeviceid }}
+
+ {{ computer.computer.azuredeviceid }}
+ Intune
+
Object ID
-
{{ computer.computer.azureobjectid }}
+
+ {{ computer.computer.azureobjectid }}
+ Entra
+
Device Category
@@ -342,6 +364,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import AssetDetailTabs from '@/components/AssetDetailTabs.vue'
import AssetRelationships from '@/components/AssetRelationships.vue'
import { useIdentifierFlags } from '@/composables/identifierSettings'
+import { getEntraDeviceUrls } from '@/utils/siteSettings'
import { setRecordTitle } from '@/composables/pageTitle'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
@@ -374,6 +397,22 @@ const assetPanels = ref(null)
// which is the answer somebody arriving from a search or a link came for.
const activegroup = ref('details')
+// Azure portal deep links, built from templates a site configures. Empty
+// templates mean no link at all, which is the default: the host differs per
+// tenant (this site is on the US Government cloud) and Microsoft moves these
+// blade paths, so guessing one would send people to a dead page.
+const entraUrlTemplate = ref('')
+const intuneUrlTemplate = ref('')
+
+function fillTemplate(template, token, value) {
+ if (!template || !value) return null
+ return template.split(token).join(encodeURIComponent(value))
+}
+const entraDeviceLink = computed(() => fillTemplate(
+ entraUrlTemplate.value, '{objectid}', computer.value?.computer?.azureobjectid))
+const intuneDeviceLink = computed(() => fillTemplate(
+ intuneUrlTemplate.value, '{deviceid}', computer.value?.computer?.azuredeviceid))
+
// Only offer a tab that has something in it. An empty tab is worse than an
// absent one - it reads as broken rather than as "this site does not collect
// that". Details always shows because core fills it regardless of plugins.
@@ -451,6 +490,13 @@ function relativeAge(value) {
}
onMounted(async () => {
+ // Non-critical: a site that has not configured these just gets no link, so a
+ // failure here must not stop the page loading.
+ getEntraDeviceUrls().then(({ entraUrl, intuneUrl }) => {
+ entraUrlTemplate.value = entraUrl
+ intuneUrlTemplate.value = intuneUrl
+ }).catch(() => {})
+
try {
const response = await computersApi.get(route.params.id)
computer.value = response.data.data
@@ -606,6 +652,18 @@ async function confirmRetire() {