Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
8ff849c
freshness: give the LLM tutorial context + SAP-aware rules
jung-thomas Aug 24, 2026
a3f7eb5
freshness: tighten system prompt for author noise + calibration
jung-thomas Aug 24, 2026
b47dbb6
Merge pull request #2012 from sap-tutorials/worktree-freshness-contex…
jung-thomas Aug 24, 2026
3f69ec7
perf(rebuild): cache node_modules to skip ~48s npm ci on unchanged lo…
jung-thomas Aug 24, 2026
18dc7db
docs(openspec): propose slug-targeted delta rebuild (O(changed) end-t…
jung-thomas Aug 24, 2026
3b58944
Merge pull request #2016 from sap-tutorials/feat/rebuild-npm-cache
jung-thomas Aug 24, 2026
5dd4d9d
Merge pull request #2017 from sap-tutorials/design/slug-targeted-delt…
jung-thomas Aug 24, 2026
874af87
fix(fetch): unmask GraphQL discovery errors + loud REST fallback
jung-thomas Aug 24, 2026
495d714
Merge pull request #2018 from sap-tutorials/fix/graphql-discovery-unmask
jung-thomas Aug 24, 2026
121f10a
feat(rebuild): generated-content cache helpers (Workstream C foundation)
jung-thomas Aug 25, 2026
7a2fb97
Merge pull request #2019 from sap-tutorials/feat/generated-content-cache
jung-thomas Aug 25, 2026
759010a
feat(rebuild): wire generated-content fast path (Workstream C, flag-g…
jung-thomas Aug 25, 2026
fff5ac9
feat(db): add ContentCurrent + ContentHistory aspects/entities (Optio…
jung-thomas Aug 25, 2026
dd85ef0
feat(publish): Option B dual-write to ContentCurrent + ContentHistory…
jung-thomas Aug 25, 2026
8b62905
Merge pull request #2020 from sap-tutorials/feat/content-cache-wiring
jung-thomas Aug 25, 2026
322f5f2
Merge pull request #2021 from sap-tutorials/feat/content-delta-publis…
jung-thomas Aug 25, 2026
d215947
fix(#2023): stop Hugo YAML 1.1 coercing yes/no quiz options to booleans
jung-thomas Aug 25, 2026
22ef08c
Merge pull request #2024 from sap-tutorials/fix/2023-yaml-yesno-coercion
jung-thomas Aug 25, 2026
cb22538
fix(notify): trigger source-repo dispatch on both master and main
jung-thomas Aug 25, 2026
38ecd0c
fix(feature-flags): register CONTENT_DELTA_WRITE_ENABLED env flag
jung-thomas Aug 25, 2026
2a3ad4d
Merge pull request #2025 from sap-tutorials/notify-master-main-trigger
jung-thomas Aug 25, 2026
f2031a4
feat(serve): Option B read cutover + rollback replay (Workstream D, f…
jung-thomas Aug 25, 2026
1060689
feat(devtoberfest): deep-link URLs for sessions grid filters/search (…
jung-thomas Aug 25, 2026
06bfb86
feat: Option B reader sweep + carry-forward removal + rollback replay…
jung-thomas Aug 25, 2026
91dc665
fix(deploy): build + copy display-app into approuter static (#2035)
jung-thomas Aug 25, 2026
c4b94d5
docs(CLAUDE): add Option B content-model gotcha (ContentCurrent + 3 f…
jung-thomas Aug 25, 2026
43b8bf4
fix(guard): mark Option B ContentCurrent slug lookups as caller-canon…
jung-thomas Aug 25, 2026
5c87e0f
chore(release): bump MTA version to 1.19.0 for PROD release (#2037)
jung-thomas Aug 25, 2026
dcd6840
feat(admin-shell): add page-search command palette (#2041) (#2043)
jung-thomas Aug 26, 2026
7354261
Turn generated-content fast path ON by default (Workstream C follow-u…
jung-thomas Aug 26, 2026
6d7d8b4
fix(devtoberfest-calendar): month grid fits at normal resolution (#2046)
jung-thomas Aug 27, 2026
a9d54c2
feat(devtoberfest): Hit the Cat mini-game + gameboard bonus/task-type…
jung-thomas Aug 27, 2026
2925cd8
fix(devtoberfest): address review — narrow unique-violation matcher; …
jung-thomas Aug 27, 2026
216b32b
chore: regenerate CDS artifacts for #2042 (last-dev csn doc-comment s…
jung-thomas Aug 27, 2026
f0c0707
feat(admin): readable week axis + total KPI + region breakdown for De…
jung-thomas Aug 27, 2026
37ebd95
Merge pull request #2048 from sap-tutorials/worktree-issue-2046-calen…
jung-thomas Aug 27, 2026
4dc6b87
Merge pull request #2050 from sap-tutorials/feat/2047-devtoberfest-si…
jung-thomas Aug 27, 2026
6d011dc
Merge pull request #2049 from sap-tutorials/worktree-hit-the-cat-2042
jung-thomas Aug 27, 2026
a21c069
feat: add /build/tags tag-taxonomy feed for CI frontmatter validation
jung-thomas Aug 27, 2026
03fdc86
Merge pull request #2051 from sap-tutorials/feat/build-tags-feed
jung-thomas Aug 27, 2026
e7b3624
feat(devtoberfest): rename mini-game "Hit the Cat" → "Tag the Cat" (#…
jung-thomas Aug 27, 2026
571f49a
Merge pull request #2052 from sap-tutorials/rename-tag-the-cat
jung-thomas Aug 27, 2026
bfdb5b7
fix(build/tags): emit mdFormat (category>value) slugs instead of disp…
jung-thomas Aug 27, 2026
ebdf5c5
Merge pull request #2053 from sap-tutorials/fix/build-tags-mdformat
jung-thomas Aug 27, 2026
997a8eb
fix(admin): readable Devtoberfest Signups report — text-arranged week…
jung-thomas Aug 27, 2026
ba38152
Merge pull request #2054 from sap-tutorials/fix/2047-signups-report-r…
jung-thomas Aug 27, 2026
80228f8
chore(release): bump MTA 1.19.0 → 1.20.0 + refresh What's New digest
jung-thomas Aug 27, 2026
46bdddd
Merge pull request #2055 from sap-tutorials/whats-new-version-bump
jung-thomas Aug 27, 2026
694ae7d
fix(parsers): comment-aware section/intro extraction
jung-thomas Aug 27, 2026
e959f96
Merge pull request #2056 from sap-tutorials/worktree-comment-aware-pa…
jung-thomas Aug 27, 2026
87dca71
perf(qa): reuse cached non-target content on QA slug-targeted rebuild…
jung-thomas Aug 27, 2026
27453d1
Merge pull request #2057 from sap-tutorials/worktree-qa-fastpath-feed…
jung-thomas Aug 27, 2026
e4b5546
chore: sync main into DEV (reconcile 1.19.0 release-merge + #2044 che…
jung-thomas Aug 27, 2026
1ebc6c6
Merge pull request #2059 from sap-tutorials/sync-main-into-dev-1.20.0
jung-thomas Aug 27, 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
2 changes: 1 addition & 1 deletion .cdsrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"target": "gen",
"tasks": [
{ "for": "hana", "src": "db", "dest": "db" },
{ "for": "nodejs", "src": "srv", "dest": "srv", "options": { "model": ["srv", "db", "app", "@cap-js/data-inspector", "@cap-js/ai/srv/AICoreService", "cds-caching/db/cache-store", "cds-caching/db/statistics", "@cap-js/attachments/srv/malware-scanner/malwareScanner-mocked"] } },
{ "for": "nodejs", "src": "srv", "dest": "srv", "options": { "model": ["srv", "db", "db/hana", "app", "@cap-js/data-inspector", "@cap-js/ai/srv/AICoreService", "cds-caching/db/cache-store", "cds-caching/db/statistics", "@cap-js/attachments/srv/malware-scanner/malwareScanner-mocked"] } },
{ "for": "hana", "src": "db-qa", "dest": "db-qa", "options": { "model": ["db-qa"] } },
{ "for": "nodejs", "src": "srv-qa", "dest": "srv-qa", "options": { "model": ["srv-qa", "db-qa"] } },
{ "for": "cds-caching" },
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.19.0
version: 1.20.0

# Top-level parameters (overridable per-env via deploy/<env>.mtaext).
parameters:
Expand Down
102 changes: 86 additions & 16 deletions app/admin-annotations.cds
Original file line number Diff line number Diff line change
Expand Up @@ -1994,20 +1994,26 @@ annotate AdminService.CompletionAnalytics with {
};
};

// --- Devtoberfest Signups (aggregated report — spec 2026-08-13) -----------
// --- Devtoberfest Signups (aggregated report — spec 2026-08-13; readable-axis
// + total-KPI + breakdowns rework for issue #2047) --------------------------
//
// Analytical List Page over the per-signup fact view. Native OData $apply drives
// the chart + analytical table + filter bar (group by week / edition / region /
// role, aggregate signup count, grand total). weekIndex is the real, portable,
// groupable time dimension; the read handler enriches each returned row with a
// human-readable weekMonday / weekLabel and — on the pure by-week series — a
// running cumulativeSignups (a window total $apply cannot compute natively).
// Those three are display-only enrichment fields, deliberately NOT dimensions or
// measures, so Fiori never tries to $apply-group or -aggregate the virtual columns.
// role, aggregate signup count, grand total).
//
// Time axis: the chart and table group+sort on the REAL Date column `weekMonday`
// (portable per dialect — db/sqlite/native.cds strftime, db/hana/native.cds
// ADD_DAYS) and DISPLAY the readable `weekStartText` label via #TextOnly text
// arrangement — so the axis reads "Mon 07 Sep 2026" (HANA) / the ISO Monday date
// (local SQLite) as a category, never the raw integer bucket or a thinned Date
// axis. The internal `weekIndex` is hidden (kept only to derive weekMonday) and
// is NOT a user-facing dimension anymore (it used to leak as "449/451" — #2047).
// weekLabel ('YYYY-Www') + cumulativeSignups are read-handler enrichment fields
// (srv/lib/devtoberfest-signup-enrich.js), display-only, never $apply dimensions.
annotate AdminService.DevtoberfestSignupAnalytics with @(
Aggregation.ApplySupported: {
Transformations: ['aggregate', 'groupby', 'filter', 'top', 'skip', 'orderby'],
GroupableProperties: [ weekIndex, eventName, eventType, region, role ],
GroupableProperties: [ weekMonday, weekStartText, eventName, eventType, region, role ],
AggregatableProperties: [ { Property: signups } ]
},
Analytics.AggregatedProperty #newSignups: {
Expand All @@ -2016,36 +2022,100 @@ annotate AdminService.DevtoberfestSignupAnalytics with @(
AggregatableProperty: signups,
![@Common.Label]: 'New Signups'
},
// Overall total registrations as a prominent, LABELLED KPI header card (#2047).
// SUM(signups) with no filter → grand total across all Devtoberfest signups.
UI.DataPoint #totalSignups: {
Value: signups,
Title: 'Total Registrations'
},
UI.PresentationVariant #totalSignups: {
Visualizations: ['@UI.DataPoint#totalSignups']
},
UI.SelectionVariant #totalSignups: {
SelectOptions: []
},
UI.KPI #totalSignups: {
SelectionVariant : ![@UI.SelectionVariant#totalSignups],
DataPoint : ![@UI.DataPoint#totalSignups],
![@UI.PresentationVariant] : ![@UI.PresentationVariant#totalSignups]
},
// Default breakdown: signups per calendar week. The chart groups on the real
// Date column weekMonday and shows its readable weekStartText label (#2047).
UI.Chart: {
ChartType: #Column,
Dimensions: [weekIndex],
Dimensions: [weekMonday],
DynamicMeasures: ['@Analytics.AggregatedProperty#newSignups']
},
UI.PresentationVariant: {
Visualizations: ['@UI.Chart', '@UI.LineItem'],
SortOrder: [{ Property: weekIndex }]
SortOrder: [{ Property: weekMonday }]
},
// Alternate breakdowns, one click away via the page's variant management. Every
// GroupableProperty is also reachable through the chart's dimension drill-down.
UI.Chart #byRegion: {
ChartType: #Column,
Dimensions: [region],
DynamicMeasures: ['@Analytics.AggregatedProperty#newSignups']
},
UI.PresentationVariant #byRegion: {
Visualizations: ['@UI.Chart#byRegion', '@UI.LineItem'],
SortOrder: [{ Property: newSignups, Descending: true }]
},
UI.SelectionPresentationVariant #byRegion: {
Text : 'By Region',
SelectionVariant : { SelectOptions: [] },
PresentationVariant : ![@UI.PresentationVariant#byRegion]
},
UI.Chart #byEdition: {
ChartType: #Column,
Dimensions: [eventName],
DynamicMeasures: ['@Analytics.AggregatedProperty#newSignups']
},
UI.PresentationVariant #byEdition: {
Visualizations: ['@UI.Chart#byEdition', '@UI.LineItem'],
SortOrder: [{ Property: newSignups, Descending: true }]
},
UI.SelectionPresentationVariant #byEdition: {
Text : 'By Edition',
SelectionVariant : { SelectOptions: [] },
PresentationVariant : ![@UI.PresentationVariant#byEdition]
},
UI.Chart #byRole: {
ChartType: #Column,
Dimensions: [role],
DynamicMeasures: ['@Analytics.AggregatedProperty#newSignups']
},
UI.PresentationVariant #byRole: {
Visualizations: ['@UI.Chart#byRole', '@UI.LineItem'],
SortOrder: [{ Property: newSignups, Descending: true }]
},
UI.SelectionPresentationVariant #byRole: {
Text : 'By Role',
SelectionVariant : { SelectOptions: [] },
PresentationVariant : ![@UI.PresentationVariant#byRole]
},
UI.SelectionFields: [ eventName, region, role ],
UI.LineItem: [
{ Value: weekLabel, Label: 'Week' },
{ Value: weekMonday, Label: 'Week Starting' },
{ Value: eventName, Label: 'Edition' },
{ Value: region, Label: 'Region' },
{ Value: role, Label: 'Role' },
{ Value: newSignups, Label: 'New Signups' },
{ Value: cumulativeSignups, Label: 'Cumulative' }
{ Value: cumulativeSignups, Label: 'Cumulative (running total)' }
]
) {
ID @UI.Hidden;
weekIndex @title: 'Week #' @Analytics.Dimension;
weekMonday @title: 'Week Starting' @Analytics.Dimension
@Common: { Text: weekStartText, TextArrangement: #TextOnly };
weekStartText @title: 'Week Starting' @Analytics.Dimension @UI.Hidden;
weekIndex @title: 'Week #' @UI.Hidden;
eventName @title: 'Edition' @Analytics.Dimension;
eventType @title: 'Event Type' @Analytics.Dimension;
region @title: 'Region' @Analytics.Dimension;
role @title: 'Role' @Analytics.Dimension;
signups @title: 'Signups' @Analytics.Measure @Aggregation.default: #SUM;
weekMonday @title: 'Week Starting';
weekLabel @title: 'Week';
cumulativeSignups @title: 'Cumulative';
weekLabel @title: 'Calendar Week';
cumulativeSignups @title: 'Cumulative (running total)';
};

// Filter-bar value help. eventName resolves against Events (edition picker);
Expand Down
209 changes: 180 additions & 29 deletions app/admin-shell/webapp/controller/Shell.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ sap.ui.define([
this._attachHashChangeDetection();
this._loadUserProfile();
this._loadVersion();

// #2041 — Ctrl+K / Cmd+K opens the page-search palette globally.
this._fnGlobalKeydown = this._onGlobalKeydown.bind(this);
document.addEventListener("keydown", this._fnGlobalKeydown);
},

onExit: function () {
if (this._fnGlobalKeydown) {
document.removeEventListener("keydown", this._fnGlobalKeydown);
this._fnGlobalKeydown = null;
}
},

onNavBack: function () {
Expand All @@ -149,39 +160,179 @@ sap.ui.define([
},

onNavItemSelect: function (oEvent) {
var sKey = oEvent.getParameter("item").getKey();
var oItem = oEvent.getParameter("item");
// Items carrying an href navigate natively via the rendered anchor; do
// NOT also dispatch in JS or external target="_blank" links open twice.
// (The search palette has no anchors, so it routes href items itself.)
if (oItem.getHref()) return;
this._navigateToNavItem({ key: oItem.getKey() });
},

// Shared navigation dispatch used by both the side navigation
// (onNavItemSelect) and the page-search palette (onSearchResultSelect),
// so external links, keyed routes, and the special deep-link hashes stay
// in exactly one place. `oItem`: { key, href, target }.
_navigateToNavItem: function (oItem) {
if (!oItem) return;

// External links (Analytics, Data Inspector, BAIP, Devtoberfest Planner)
// carry an href but no route — navigate the browser directly. The side
// nav renders these as native anchors; the palette has to do it by hand.
if (oItem.href) {
if (oItem.target === "_blank") {
window.open(oItem.href, "_blank", "noopener");
} else {
window.location.assign(oItem.href);
}
return;
}

var sKey = oItem.key;
if (!sKey) return;

var sRoute = NAV_KEY_TO_ROUTE[sKey];
if (sRoute) {
this.getOwnerComponent().getRouter().navTo(sRoute);
if (sKey === "pipelinelog") {
HashChanger.getInstance().setHash("pipelinelog&/op/PipelineLog");
}
if (sKey === "joblog") {
HashChanger.getInstance().setHash("joblog&/op/JobExecutionLog");
}
if (sKey === "homepageRedirects") {
HashChanger.getInstance().setHash("homepageRedirects&/hp/Redirects");
}
if (sKey === "homepageConfig") {
// Fixed singleton UUID; matches auto-init handler at
// srv/admin-service.js:601 (HOMEPAGE_CONFIG_SINGLETON_ID).
// HomepageConfig used to be @odata.singleton but that combo is
// incompatible with @odata.draft.enabled (draftActivate requires
// the ID key), so it was demoted to a keyed collection with a
// single well-known row. See srv/admin-service.cds header comment.
HashChanger.getInstance().setHash("homepageConfig&/hp/HomepageConfig(00000000-0000-0000-0000-00000000c8ae)");
}
if (sKey === "petoberfestContests") {
// Deep-link into the petoberfest componentUsage's inner "Petoberfests"
// List Report route (contest maintenance). The bare "petoberfest" route
// lands on the PetSubmissions moderation queue; this second outer route
// shares the same componentUsage target (prefix "pb") and drives the
// inner hash to the contest LR — mirrors the pipelinelog/joblog pattern
// above that reuses the operations target. (#1449)
HashChanger.getInstance().setHash("petoberfestContests&/pb/Petoberfests");
if (!sRoute) return;

this.getOwnerComponent().getRouter().navTo(sRoute);
if (sKey === "pipelinelog") {
HashChanger.getInstance().setHash("pipelinelog&/op/PipelineLog");
}
if (sKey === "joblog") {
HashChanger.getInstance().setHash("joblog&/op/JobExecutionLog");
}
if (sKey === "homepageRedirects") {
HashChanger.getInstance().setHash("homepageRedirects&/hp/Redirects");
}
if (sKey === "homepageConfig") {
// Fixed singleton UUID; matches auto-init handler at
// srv/admin-service.js:601 (HOMEPAGE_CONFIG_SINGLETON_ID).
// HomepageConfig used to be @odata.singleton but that combo is
// incompatible with @odata.draft.enabled (draftActivate requires
// the ID key), so it was demoted to a keyed collection with a
// single well-known row. See srv/admin-service.cds header comment.
HashChanger.getInstance().setHash("homepageConfig&/hp/HomepageConfig(00000000-0000-0000-0000-00000000c8ae)");
}
if (sKey === "petoberfestContests") {
// Deep-link into the petoberfest componentUsage's inner "Petoberfests"
// List Report route (contest maintenance). The bare "petoberfest" route
// lands on the PetSubmissions moderation queue; this second outer route
// shares the same componentUsage target (prefix "pb") and drives the
// inner hash to the contest LR — mirrors the pipelinelog/joblog pattern
// above that reuses the operations target. (#1449)
HashChanger.getInstance().setHash("petoberfestContests&/pb/Petoberfests");
}
},

// ---- Page search palette (#2041) -------------------------------------
// A command-palette style popover for jumping to any admin page. Flattens
// the (already role-filtered) nav model into a searchable flat list, so an
// author never sees admin-only pages in results.

onOpenPageSearch: function (oEvent) {
this._openPageSearch(oEvent.getSource());
},

_openPageSearch: function (oOpenerControl) {
var oPopover = this.byId("pageSearchPopover");
if (!oPopover) return;
// Rebuild the flat catalog every open — role filtering may have applied
// after the previous open, and it is cheap (~55 rows).
var oSearchModel = this._getSearchModel();
oSearchModel.setProperty("/all", this._flattenNav());
oSearchModel.setProperty("/query", "");
this._applySearchFilter("");
if (oPopover.isOpen()) return;
oPopover.openBy(oOpenerControl || this.byId("pageSearchBtn"));
},

onPageSearchAfterOpen: function () {
var oField = this.byId("pageSearchField");
if (oField) oField.focus();
},

onPageSearchLiveChange: function (oEvent) {
this._applySearchFilter(oEvent.getParameter("newValue") || "");
},

// Enter in the SearchField activates the first (top) result.
onPageSearchGo: function () {
var aResults = this._getSearchModel().getProperty("/results") || [];
if (aResults.length) {
this._selectSearchResult(aResults[0]);
}
},

onSearchResultSelect: function (oEvent) {
var oCtx = oEvent.getSource().getBindingContext("search");
if (!oCtx) return;
this._selectSearchResult(oCtx.getObject());
},

_selectSearchResult: function (oResult) {
var oPopover = this.byId("pageSearchPopover");
if (oPopover && oPopover.isOpen()) oPopover.close();
this._navigateToNavItem(oResult);
},

_getSearchModel: function () {
var oModel = this.getView().getModel("search");
if (!oModel) {
oModel = new JSONModel({ query: "", all: [], results: [] });
this.getView().setModel(oModel, "search");
}
return oModel;
},

// Flatten the role-filtered nav tree into { key, title, groupTitle, icon,
// href, target }. Container groups contribute their leaves; a top-level
// leaf (e.g. Dashboard) contributes itself.
_flattenNav: function () {
var oNavModel = this.getOwnerComponent().getModel("nav");
var aGroups = (oNavModel && oNavModel.getData().groups) || [];
var aFlat = [];
aGroups.forEach(function (g) {
if (g.items && g.items.length) {
g.items.forEach(function (leaf) {
aFlat.push({
key: leaf.key,
title: leaf.title,
groupTitle: g.title,
icon: g.icon,
href: leaf.href,
target: leaf.target
});
});
} else {
aFlat.push({
key: g.key,
title: g.title,
groupTitle: g.title,
icon: g.icon,
href: g.href,
target: g.target
});
}
});
return aFlat;
},

_applySearchFilter: function (sQuery) {
var oModel = this._getSearchModel();
var aAll = oModel.getProperty("/all") || [];
var q = (sQuery || "").trim().toLowerCase();
var aResults = !q ? aAll : aAll.filter(function (item) {
return (item.title || "").toLowerCase().indexOf(q) !== -1
|| (item.groupTitle || "").toLowerCase().indexOf(q) !== -1;
});
oModel.setProperty("/query", sQuery);
oModel.setProperty("/results", aResults);
},

_onGlobalKeydown: function (e) {
// Ctrl+K / Cmd+K opens the page search from anywhere in the shell.
if ((e.ctrlKey || e.metaKey) && !e.altKey && (e.key === "k" || e.key === "K")) {
e.preventDefault();
this._openPageSearch(this.byId("pageSearchBtn"));
}
},

Expand Down
4 changes: 4 additions & 0 deletions app/admin-shell/webapp/i18n/i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ documentTitle.anonymous=No Access
noAccess.heading=You don't have access to this console.
noAccess.body=This console requires the Admin or Tutorial.Author scope.
noAccess.requestAccess=Request access

pageSearch.tooltip=Search pages (Ctrl+K)
pageSearch.placeholder=Search pages...
pageSearch.noResults=No matching pages
Loading
Loading