Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
312e8ce
perf(build): skip AI-author phase on deploy builds (#2761)
jung-thomas Oct 14, 2026
5aa38a0
fix(scheduler): reap NULL-lastAttemptTimestamp outbox tombstones (#2782)
jung-thomas Oct 14, 2026
f50765a
Merge pull request #2783 from sap-tutorials/worktree-issue-2761-skip-…
jung-thomas Oct 14, 2026
95b35ea
Merge pull request #2784 from sap-tutorials/worktree-outbox-tombstone…
jung-thomas Oct 14, 2026
29d17b1
fix(parsers): eliminate fenceHealth false positives (#2772 follow-up)
jung-thomas Oct 14, 2026
5d83466
Merge pull request #2785 from sap-tutorials/worktree-lint-fence-false…
jung-thomas Oct 14, 2026
91f9e59
fix(devtoberfest): include Tag the Cat 100-pt bonus in total possible…
jung-thomas Oct 15, 2026
1a38007
Merge pull request #2789 from sap-tutorials/worktree-devtoberfest-max…
jung-thomas Oct 15, 2026
fe8724a
docs: URL health check design (#2787)
jung-thomas Oct 15, 2026
d8e4d74
fix(parsers): preserve video iframe in intro + retire dead golden-ren…
jung-thomas Oct 15, 2026
4fd2ce5
docs: URL health check implementation plan (#2787)
jung-thomas Oct 15, 2026
292dfa9
Merge pull request #2790 from sap-tutorials/worktree-fix-intro-iframe
jung-thomas Oct 15, 2026
35185ed
refactor: extract shared link-health probe engine (#2787)
jung-thomas Oct 15, 2026
223ef72
feat(db): link-health columns on Tutorials/Missions/Groups (#2787)
jung-thomas Oct 15, 2026
0d9aa66
feat: findUnhealthyEntities flags deleted + not-served children (#2787)
jung-thomas Oct 15, 2026
c59f093
feat: health URL resolver + target collection (#2787)
jung-thomas Oct 15, 2026
328cbf3
feat: url-health-check job (#2787)
jung-thomas Oct 15, 2026
5ae3a91
feat: register url-health-check scheduler job (#2787)
jung-thomas Oct 15, 2026
b664d50
feat(admin): expose link-health columns + runUrlHealthScan action (#2…
jung-thomas Oct 15, 2026
6a1fcbb
fix(admin): runUrlHealthScan ok reflects real outcome (#2787)
jung-thomas Oct 15, 2026
263cb08
feat(admin-ui): URL health screen with URL-status + membership tabs (…
jung-thomas Oct 15, 2026
149a53f
fix(admin-ui): render membership tab + drop undefined SelectionVarian…
jung-thomas Oct 15, 2026
aafb61c
fix(admin-ui): health Component extends UIComponent to match standalo…
jung-thomas Oct 15, 2026
23ad369
test: bump scheduler lockstep count for url-health-check (#2787)
jung-thomas Oct 15, 2026
be37de9
fix(admin-ui): health status colors + filter URL Status to scanned ro…
jung-thomas Oct 15, 2026
7dd0159
build(cds): regenerate staged csn for LinkHealth columns (#2787)
jung-thomas Oct 15, 2026
16c7e10
Merge pull request #2791 from sap-tutorials/worktree-url-health-check
jung-thomas Oct 15, 2026
22d2b0e
fix(transcript): prefer English caption track for AI quiz generation
jung-thomas Oct 15, 2026
29af51c
Merge pull request #2792 from sap-tutorials/fix/transcript-caption-lang
jung-thomas Oct 15, 2026
bc7b85a
fix(ci): valid LinkHealth default + wire health nav-key (#2787)
jung-thomas Oct 15, 2026
439336d
Merge pull request #2793 from sap-tutorials/worktree-fix-linkhealth-enum
jung-thomas Oct 15, 2026
2cd60b6
fix(cds): regenerate LinkHealth migration tables + harden build (#2787)
jung-thomas Oct 15, 2026
b1bac3f
chore(release): 1.39.0
jung-thomas Oct 15, 2026
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
10 changes: 0 additions & 10 deletions .cdsrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,6 @@
},
{
"for": "cds-caching"
},
{
"for": "hana",
"src": "db",
"dest": "db",
"options": {
"model": [
"cds-caching/db/statistics"
]
}
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion .deploy/mta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ID: tutorials-ims
# Bump this on each release you deploy — it's the version shown by `cf mtas`
# and in the mtar filename (tutorials-ims_<version>.mtar). Deploy is manual:
# `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_<version>.mtar -e ../deploy/<env>.mtaext -f`.
version: 1.38.0
version: 1.39.0

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
1 change: 1 addition & 0 deletions app/admin-shell/webapp/controller/Shell.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ sap.ui.define([
channelTopicMap: "channelTopicMap",
channelSubmissions: "channelSubmissions",
semaphoreConfig: "semaphoreConfig",
health: "health",
quarantineEvents: "quarantineEvents"
};

Expand Down
26 changes: 25 additions & 1 deletion app/admin-shell/webapp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@
"sap.tutorials.admin.channels": "./components/channels",
"sap.tutorials.admin.channelCollections": "./components/channel-collections",
"sap.tutorials.admin.channelTopicMap": "./components/channel-topic-map",
"sap.tutorials.admin.channelSubmissions": "./components/channel-submissions"
"sap.tutorials.admin.channelSubmissions": "./components/channel-submissions",
"sap.tutorials.admin.health": "./components/health"
},
"models": {
"admin": {
Expand Down Expand Up @@ -435,6 +436,12 @@
"settings": {},
"componentData": {},
"lazy": true
},
"healthComponent": {
"name": "sap.tutorials.admin.health",
"settings": {},
"componentData": {},
"lazy": true
}
},
"services": {
Expand Down Expand Up @@ -1025,6 +1032,16 @@
}
]
},
{
"name": "health",
"pattern": "health",
"target": [
{
"name": "healthTarget",
"prefix": "he"
}
]
},
{
"name": "feedbackDashboard",
"pattern": "feedback/dashboard",
Expand Down Expand Up @@ -1424,6 +1441,13 @@
"viewLevel": 1,
"prefix": "csub"
},
"healthTarget": {
"type": "Component",
"usage": "healthComponent",
"id": "healthTarget",
"viewLevel": 1,
"prefix": "he"
},
"feedbackDashboardTarget": {
"viewName": "TutorialFeedbackDashboard",
"viewLevel": 1
Expand Down
3 changes: 2 additions & 1 deletion app/admin-shell/webapp/model/navigation.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
{ "key": "tutorialSurvey", "title": "Tutorial Survey", "href": "/analytics-ui/#/reports/survey", "target": "_self", "requiredScope": "Tutorial.Author" },
{ "key": "statistics", "title": "Statistics" },
{ "key": "metrics", "title": "Metrics" },
{ "key": "dataExport", "title": "Data Export" }
{ "key": "dataExport", "title": "Data Export" },
{ "key": "health", "title": "URL Health", "icon": "sap-icon://chain-link", "requiredScope": "Admin" }
]
},
{
Expand Down
6 changes: 6 additions & 0 deletions app/admin/health/webapp/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sap.ui.define(["sap/ui/core/UIComponent"], function (UIComponent) {
"use strict";
return UIComponent.extend("sap.tutorials.admin.health.Component", {
metadata: { manifest: "json" }
});
});
67 changes: 67 additions & 0 deletions app/admin/health/webapp/controller/Health.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel"
], function (Controller, JSONModel) {
"use strict";

return Controller.extend("sap.tutorials.admin.health.controller.Health", {

onInit: function () {
// Initialise health state model (scan result message).
this.getView().setModel(new JSONModel({ scanMessage: "" }), "health");

// Load membership issues from completionHealth() unbound function.
var that = this;
this.getView().setModel(new JSONModel({ issues: [] }), "membership");
fetch("/admin/completionHealth()", { credentials: "include", headers: { Accept: "application/json" } })
.then(function (r) {
if (!r.ok) { throw new Error("HTTP " + r.status); }
return r.json();
})
.then(function (d) {
that.getView().getModel("membership").setData({ issues: d.value || d });
})
.catch(function (e) {
sap.ui.require(["sap/m/MessageToast"], function (T) {
T.show("Membership check failed: " + e.message);
});
});
},

// Calls the unbound OData action runUrlHealthScan() and shows the result.
onRunScan: function () {
var that = this;
var oModel = this.getView().getModel();
var oHealthModel = this.getView().getModel("health");

this.byId("runScanBtn").setEnabled(false);
oHealthModel.setProperty("/scanMessage", "Running scan…");

var oOperation = oModel.bindContext("/runUrlHealthScan(...)");
oOperation.execute().then(function () {
var oResult = oOperation.getBoundContext().getObject();
var sMsg = oResult.message ||
("Scan complete — ok: " + oResult.ok + ", broken: " + oResult.broken);
oHealthModel.setProperty("/scanMessage", sMsg);
// Refresh the URL status table binding.
var oTable = that.byId("urlStatusTable");
if (oTable) { oTable.getBinding("items").refresh(); }
}).catch(function (e) {
oHealthModel.setProperty("/scanMessage", "Scan failed: " + (e.message || e));
sap.ui.require(["sap/m/MessageBox"], function (MB) {
MB.error("Scan failed: " + (e.message || e));
});
}).finally(function () {
that.byId("runScanBtn").setEnabled(true);
});
},

// Formats the badSlots array into a readable comma-joined string of reasons.
formatBadSlots: function (aBadSlots) {
if (!Array.isArray(aBadSlots) || aBadSlots.length === 0) { return "—"; }
return aBadSlots.map(function (s) {
return s.reason || s.slot || JSON.stringify(s);
}).join(", ");
}
});
});
54 changes: 54 additions & 0 deletions app/admin/health/webapp/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"_version": "1.65.0",
"sap.app": {
"id": "sap.tutorials.admin.health",
"type": "application",
"applicationVersion": { "version": "1.0.0" },
"title": "URL Health",
"crossNavigation": {
"inbounds": {
"Health-display": {
"semanticObject": "Health",
"action": "display",
"title": "URL Health"
}
}
},
"dataSources": {
"adminService": {
"uri": "/admin/",
"type": "OData",
"settings": { "odataVersion": "4.0" }
}
}
},
"sap.ui5": {
"rootView": {
"viewName": "sap.tutorials.admin.health.view.Health",
"type": "XML",
"id": "health",
"async": true
},
"dependencies": {
"minUI5Version": "1.148.0",
"libs": {
"sap.m": {},
"sap.ui.core": {},
"sap.ui.model.odata.v4": {}
}
},
"models": {
"": {
"dataSource": "adminService",
"preload": true,
"settings": {
"synchronizationMode": "None",
"operationMode": "Server",
"autoExpandSelect": true,
"earlyRequests": true
}
}
},
"contentDensities": { "compact": true, "cozy": true }
}
}
119 changes: 119 additions & 0 deletions app/admin/health/webapp/view/Health.view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<mvc:View
controllerName="sap.tutorials.admin.health.controller.Health"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
xmlns:core="sap.ui.core"
height="100%">
<ScrollContainer height="100%" width="100%" vertical="true" horizontal="false">
<VBox class="sapUiSmallMargin">
<Title text="URL Health" level="H2" class="sapUiSmallMarginBottom" />

<Toolbar class="sapUiSmallMarginBottom">
<Button
id="runScanBtn"
text="Run scan now"
type="Emphasized"
press=".onRunScan"
icon="sap-icon://chain-link" />
<ToolbarSeparator />
<Text id="scanResultText" text="{health>/scanMessage}" visible="{= !!${health>/scanMessage} }" />
</Toolbar>

<IconTabBar
id="healthTabs"
expanded="true"
applyContentPadding="false"
class="sapUiSmallMarginBottom">
<items>
<IconTabFilter key="urlStatus" text="URL Status" icon="sap-icon://monitor-payments">
<Table
id="urlStatusTable"
growing="true"
growingThreshold="50"
busyIndicatorDelay="0"
items="{
path: '/Tutorials',
parameters: {
$select: 'slug,linkStatus,lastChecked,linkStatusOverride',
$filter: 'linkStatus ne null',
$orderby: 'lastChecked desc'
}
}">
<headerToolbar>
<OverflowToolbar>
<Title text="Tutorials — URL Status" level="H3" />
</OverflowToolbar>
</headerToolbar>
<noData>
<IllustratedMessage
illustrationType="sapIllus-NoEntries"
title="No scan data yet"
description="Run a scan to populate link-status results." />
</noData>
<columns>
<Column width="14rem"><Text text="Slug" /></Column>
<Column width="8rem"><Text text="Link Status" /></Column>
<Column width="12rem"><Text text="Last Checked" /></Column>
<Column><Text text="Override" /></Column>
</columns>
<items>
<ColumnListItem>
<cells>
<Text text="{slug}" />
<ObjectStatus
text="{linkStatus}"
state="{= ${linkStatus} === 'OK' ? 'Success' : ${linkStatus} === 'BROKEN' ? 'Error' : ${linkStatus} === 'SLOW' ? 'Warning' : 'None' }" />
<Text text="{= ${lastChecked} ? new Date(${lastChecked}).toLocaleString() : '—' }" />
<Input value="{linkStatusOverride}" placeholder="(none)" />
</cells>
</ColumnListItem>
</items>
</Table>
</IconTabFilter>

<IconTabFilter key="membership" text="Membership Issues" icon="sap-icon://alert">
<Table
id="membershipTable"
growing="true"
growingThreshold="50"
busyIndicatorDelay="0"
items="{membership>/issues}">
<headerToolbar>
<OverflowToolbar>
<Title text="Membership Issues" level="H3" />
</OverflowToolbar>
</headerToolbar>
<noData>
<IllustratedMessage
illustrationType="sapIllus-NoEntries"
title="No membership issues"
description="No completion-health issues found." />
</noData>
<columns>
<Column width="8rem"><Text text="Kind" /></Column>
<Column><Text text="Title" /></Column>
<Column width="12rem"><Text text="Slug" /></Column>
<Column><Text text="Bad Slots (reasons)" /></Column>
</columns>
<items>
<ColumnListItem>
<cells>
<ObjectStatus text="{membership>kind}" state="Warning" />
<Text text="{membership>title}" wrapping="false" />
<Text text="{membership>slug}" />
<Text
text="{
path: 'membership>badSlots',
formatter: '.formatBadSlots'
}"
wrapping="true" />
</cells>
</ColumnListItem>
</items>
</Table>
</IconTabFilter>
</items>
</IconTabBar>
</VBox>
</ScrollContainer>
</mvc:View>
Loading
Loading