add "what's new" popup - resolves #670

Sat, 17 May 2025 17:39:48 +0200

author
Mike Becker <universe@uap-core.de>
date
Sat, 17 May 2025 17:39:48 +0200
changeset 367
0a9065936aac
parent 366
b351e70ab325
child 368
93a5d42d941f

add "what's new" popup - resolves #670

build.gradle.kts file | annotate | diff | comparison | revisions
setup/postgres/psql_create_tables.sql file | annotate | diff | comparison | revisions
setup/postgres/psql_patch_1.5.0.sql file | annotate | diff | comparison | revisions
setup/postgres/psql_patch_1.6.0.sql file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/Constants.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt file | annotate | diff | comparison | revisions
src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt file | annotate | diff | comparison | revisions
src/main/resources/localization/strings.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/strings_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog-de.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/changelogs/changelog.jspf file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
--- a/build.gradle.kts	Fri Mar 14 08:09:05 2025 +0100
+++ b/build.gradle.kts	Sat May 17 17:39:48 2025 +0200
@@ -5,7 +5,7 @@
     war
 }
 group = "de.uapcore"
-version = "1.5.1"
+version = "1.6.0"
 
 repositories {
     mavenCentral()
--- a/setup/postgres/psql_create_tables.sql	Fri Mar 14 08:09:05 2025 +0100
+++ b/setup/postgres/psql_create_tables.sql	Sat May 17 17:39:48 2025 +0200
@@ -1,10 +1,11 @@
 create table lpit_user
 (
-    userid    serial primary key,
-    username  text not null unique,
-    mail      text,
-    lastname  text,
-    givenname text
+    userid              serial primary key,
+    username            text                     not null unique,
+    mail                text,
+    lastname            text,
+    givenname           text,
+    knows_updates_until timestamp with time zone
 );
 
 create type vcstype as enum ('None', 'Mercurial', 'Git');
--- a/setup/postgres/psql_patch_1.5.0.sql	Fri Mar 14 08:09:05 2025 +0100
+++ b/setup/postgres/psql_patch_1.5.0.sql	Sat May 17 17:39:48 2025 +0200
@@ -1,4 +1,4 @@
--- apply this script to patch a version < 1.5.0 database to version 1.5.0
+-- apply this script to patch a version 1.1.0 database to version 1.5.0
 
 alter table lpit_issue_history_event
     add userid integer null references lpit_user (userid) on delete set null;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup/postgres/psql_patch_1.6.0.sql	Sat May 17 17:39:48 2025 +0200
@@ -0,0 +1,3 @@
+-- apply this script to patch a version 1.5.0 database to version 1.6.0
+
+alter table lpit_user add knows_updates_until timestamp with time zone;
--- a/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/AbstractServlet.kt	Sat May 17 17:39:48 2025 +0200
@@ -28,6 +28,7 @@
 import de.uapcore.lightpit.DataSourceProvider.Companion.SC_ATTR_NAME
 import de.uapcore.lightpit.dao.DataAccessObject
 import de.uapcore.lightpit.dao.createDataAccessObject
+import de.uapcore.lightpit.entities.User
 import jakarta.servlet.http.Cookie
 import jakarta.servlet.http.HttpServlet
 import jakarta.servlet.http.HttpServletRequest
@@ -35,6 +36,7 @@
 import java.sql.SQLException
 import java.time.ZoneId
 import java.util.*
+import java.sql.Date as SqlDate
 
 abstract class AbstractServlet : HttpServlet() {
 
@@ -89,8 +91,10 @@
     ) {
         val params = mapping.first.obtainPathParameters(sanitizedRequestPath(req))
         val method = mapping.second
+        val authenticatedUser = req.remoteUser?.let(dao::findUserByName)
+        showWhatsNewPopup(authenticatedUser, req, dao)
         logger.trace("invoke {0}", method)
-        method(HttpRequest(req, resp, params), dao)
+        method(HttpRequest(authenticatedUser, req, resp, params), dao)
     }
 
     private fun sanitizedRequestPath(req: HttpServletRequest) = req.pathInfo ?: "/"
@@ -124,9 +128,7 @@
         req.characterEncoding = "UTF-8"
 
         // set some internal request attributes
-        val http = HttpRequest(req, resp)
         val fullPath = req.servletPath + Optional.ofNullable(req.pathInfo).orElse("")
-        req.setAttribute(Constants.REQ_ATTR_BASE_HREF, http.baseHref)
         req.setAttribute(Constants.REQ_ATTR_PATH, fullPath)
         req.getHeader("Referer")?.let {
             // TODO: add a sanity check to avoid link injection
@@ -136,7 +138,7 @@
         // choose the requested language as session language (if available)
         if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
             // language selection stored in cookie
-            val cookieLocale = cookieLanguage(http)
+            val cookieLocale = cookieLanguage(req)
 
             // if no cookie, fall back to request locale a.k.a "Browser Language"
             val reqLocale = cookieLocale ?: req.locale
@@ -145,7 +147,7 @@
             val sessionLocale = if (availableLanguages.contains(reqLocale)) reqLocale else availableLanguages.first()
 
             // select the language (this will also refresh the cookie max-age)
-            selectLanguage(http, sessionLocale)
+            selectLanguage(req, resp, sessionLocale)
 
             logger.debug(
                 "Setting language for new session {0}: {1}", session.id, sessionLocale.displayLanguage
@@ -159,17 +161,18 @@
         // determine the timezone
         if (session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) == null) {
             // timezone selection stored in cookie
-            val cookieTimezone = cookieTimezone(http)
+            val cookieTimezone = cookieTimezone(req)
 
             // if no cookie, fall back to server's timezone (the browser does not transmit one)
             val timezone = cookieTimezone ?: ZoneId.systemDefault()
 
-            selectTimezone(http, timezone)
+            selectTimezone(req, resp, timezone)
             logger.debug("Timezone for session {0} set to {1}", session.id, timezone)
         }
 
         // if this is an error path, bypass the normal flow
         if (fullPath.startsWith("/error/")) {
+            val http = HttpRequest(null, req, resp)
             http.styleSheets = listOf("error")
             http.render("error")
             return
@@ -210,6 +213,16 @@
         }
     }
 
+    private fun showWhatsNewPopup(user: User?, req: HttpServletRequest, dao: DataAccessObject) {
+        if (user == null) return
+        logger.trace("show user with ID {0} what's new", user.id)
+        val userKnowsUpdatesUntil = dao.untilWhenUserKnowsUpdates(user)
+        if (userKnowsUpdatesUntil == null || userKnowsUpdatesUntil.before(SqlDate.valueOf(Constants.VERSION_DATE))) {
+            dao.updateUserKnowsUpdates(user)
+            req.setAttribute("showWhatsNew", true)
+        }
+    }
+
     override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
         doProcess(req, resp, getMappings)
     }
@@ -224,40 +237,48 @@
         return locales.ifEmpty { listOf(Locale.ENGLISH) }
     }
 
-    private fun cookieLanguage(http: HttpRequest): Locale? =
-        http.request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME }
+    private fun cookieLanguage(request: HttpServletRequest): Locale? =
+        request.cookies?.firstOrNull { c -> c.name == LANGUAGE_COOKIE_NAME }
             ?.runCatching {Locale.forLanguageTag(this.value)}?.getOrNull()
 
     protected fun sessionLanguage(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) as Locale
 
-    private fun cookieTimezone(http: HttpRequest): ZoneId? =
-        http.request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME }
+    private fun cookieTimezone(request: HttpServletRequest): ZoneId? =
+        request.cookies?.firstOrNull { c -> c.name == TIMEZONE_COOKIE_NAME }
             ?.runCatching { ZoneId.of(this.value)}?.getOrNull()
 
     protected fun sessionTimezone(http: HttpRequest) = http.session.getAttribute(Constants.SESSION_ATTR_TIMEZONE) as String
 
     protected fun selectTimezone(http: HttpRequest, zoneId: ZoneId) {
-        http.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id)
+        selectTimezone(http.request, http.response, zoneId)
+    }
+
+    private fun selectTimezone(request: HttpServletRequest, response: HttpServletResponse, zoneId: ZoneId) {
+        request.session.setAttribute(Constants.SESSION_ATTR_TIMEZONE, zoneId.id)
         val cookie = Cookie(TIMEZONE_COOKIE_NAME, zoneId.id)
         cookie.isHttpOnly = true
-        cookie.path = http.request.contextPath
+        cookie.path = request.contextPath
         cookie.maxAge = COOKIE_MAX_AGE
-        http.response.addCookie(cookie)
+        response.addCookie(cookie)
     }
 
     protected fun selectLanguage(http: HttpRequest, locale: Locale) {
-        http.response.locale = locale
-        http.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale)
+        selectLanguage(http.request, http.response, locale)
+    }
+
+    private fun selectLanguage(request: HttpServletRequest, response: HttpServletResponse, locale: Locale) {
+        response.locale = locale
+        request.session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, locale)
         // delete cookie if language selection matches request locale, otherwise set cookie
         val cookie = Cookie(LANGUAGE_COOKIE_NAME, "")
         cookie.isHttpOnly = true
-        cookie.path = http.request.contextPath
-        if (http.request.locale.language == locale.language) {
+        cookie.path = request.contextPath
+        if (request.locale.language == locale.language) {
             cookie.maxAge = 0
         } else {
             cookie.value = locale.language
             cookie.maxAge = COOKIE_MAX_AGE
         }
-        http.response.addCookie(cookie)
+        response.addCookie(cookie)
     }
 }
--- a/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/Constants.kt	Sat May 17 17:39:48 2025 +0200
@@ -26,6 +26,8 @@
 package de.uapcore.lightpit
 
 object Constants {
+    const val VERSION_DATE = "2025-05-17"
+
     /**
      * The path where the JSP files reside.
      */
--- a/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/RequestMapping.kt	Sat May 17 17:39:48 2025 +0200
@@ -27,6 +27,7 @@
 
 import de.uapcore.lightpit.dao.DataAccessObject
 import de.uapcore.lightpit.entities.HasNode
+import de.uapcore.lightpit.entities.User
 import de.uapcore.lightpit.viewmodel.NavMenu
 import de.uapcore.lightpit.viewmodel.View
 import jakarta.servlet.http.HttpServletRequest
@@ -52,14 +53,13 @@
 class ValidatedValue<T>(val result: T): ValidationResult<T>
 
 class HttpRequest(
+    val user: User?,
     val request: HttpServletRequest,
     val response: HttpServletResponse,
     val pathParams: PathParameters = emptyMap()
 ) {
     val session: HttpSession = request.session
 
-    val remoteUser: String? = request.remoteUser
-
     /**
      * The name of the content page.
      *
@@ -153,6 +153,10 @@
      */
     val baseHref get() = "${request.scheme}://${request.serverName}$portInfo${request.contextPath}/"
 
+    init {
+        request.setAttribute(Constants.REQ_ATTR_BASE_HREF, baseHref)
+    }
+
     private fun String.withExt(ext: String) = if (endsWith(ext)) this else plus(ext)
     private fun jspPath(name: String) = Constants.JSP_PATH_PREFIX.plus(name).withExt(".jsp")
 
--- a/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/DataAccessObject.kt	Sat May 17 17:39:48 2025 +0200
@@ -31,6 +31,7 @@
 import de.uapcore.lightpit.viewmodel.IssueSummary
 import de.uapcore.lightpit.viewmodel.VariantSummary
 import de.uapcore.lightpit.viewmodel.VersionSummary
+import java.sql.Date
 
 interface DataAccessObject {
 
@@ -39,6 +40,8 @@
     fun findUserByName(username: String): User?
     fun insertUser(user: User)
     fun updateUser(user: User)
+    fun untilWhenUserKnowsUpdates(user: User): Date?
+    fun updateUserKnowsUpdates(user: User)
 
     /**
      * Lists all versions of the specified [project].
--- a/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/dao/PostgresDataAccessObject.kt	Sat May 17 17:39:48 2025 +0200
@@ -33,6 +33,7 @@
 import de.uapcore.lightpit.viewmodel.VersionSummary
 import org.intellij.lang.annotations.Language
 import java.sql.Connection
+import java.sql.Date
 import java.sql.PreparedStatement
 import java.sql.ResultSet
 
@@ -129,6 +130,21 @@
             executeUpdate()
         }
     }
+
+    override fun untilWhenUserKnowsUpdates(user: User): Date? =
+        withStatement("select knows_updates_until from lpit_user where userid = ?") {
+            setInt(1, user.id)
+            querySingle { it.getDate(1) }
+        }
+
+    override fun updateUserKnowsUpdates(user: User) {
+        withStatement("update lpit_user set knows_updates_until = now() where userid = ?") {
+            with(user) {
+                setInt(1, id)
+            }
+            executeUpdate()
+        }
+    }
     //</editor-fold>
 
     //<editor-fold desc="Version">
--- a/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/logic/IssueLogic.kt	Sat May 17 17:39:48 2025 +0200
@@ -82,19 +82,18 @@
 }
 
 fun processIssueForm(issue: Issue, reference: Issue, http: HttpRequest, dao: DataAccessObject) {
-    val remoteUser = http.remoteUser?.let { dao.findUserByName(it) }
     if (issue.hasChanged(reference)) {
         dao.updateIssue(issue)
-        dao.insertHistoryEvent(remoteUser, issue)
+        dao.insertHistoryEvent(http.user, issue)
     }
     val newComment = http.param("comment")
     if (!newComment.isNullOrBlank()) {
         val comment = IssueComment(-1, issue.id).apply {
-            author = remoteUser
+            author = http.user
             comment = newComment
         }
         val commentid = dao.insertComment(comment)
-        dao.insertHistoryEvent(remoteUser, issue, comment, commentid)
+        dao.insertHistoryEvent(http.user, issue, comment, commentid)
     }
 }
 
@@ -110,7 +109,6 @@
 }
 
 fun processIssueComment(issue:Issue, http: HttpRequest, dao: DataAccessObject): Boolean {
-    val remoteUser = http.remoteUser?.let { dao.findUserByName(it) }
     val commentId = http.param("commentid")?.toIntOrNull() ?: -1
     if (commentId > 0) {
         val comment = dao.findComment(commentId)
@@ -118,12 +116,12 @@
             http.response.sendError(404)
             return false
         }
-        if (comment.author != null && comment.author?.id == remoteUser?.id) {
+        if (comment.author != null && comment.author?.id == http.user?.id) {
             val newComment = http.param("comment")
             if (!newComment.isNullOrBlank()) {
                 comment.comment = newComment
                 dao.updateComment(comment)
-                dao.insertHistoryEvent(remoteUser, issue, comment)
+                dao.insertHistoryEvent(http.user, issue, comment)
             }
         } else {
             http.response.sendError(403)
@@ -131,11 +129,11 @@
         }
     } else {
         val comment = IssueComment(-1, issue.id).apply {
-            author = remoteUser
+            author = http.user
             comment = http.param("comment") ?: ""
         }
         val newId = dao.insertComment(comment)
-        dao.insertHistoryEvent(remoteUser, issue, comment, newId)
+        dao.insertHistoryEvent(http.user, issue, comment, newId)
     }
     return true
 }
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/IssuesServlet.kt	Sat May 17 17:39:48 2025 +0200
@@ -32,7 +32,7 @@
 
         val issues = dao.listIssues(filter.includeDone)
             .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
-            .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "<Anonymous>"))
+            .filter(issueFilterFunction(filter, relationsMap, http.user?.username ?: "<Anonymous>"))
 
         with(http) {
             pageTitle = i18n("issues")
--- a/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/kotlin/de/uapcore/lightpit/servlet/ProjectServlet.kt	Sat May 17 17:39:48 2025 +0200
@@ -125,7 +125,7 @@
                 specificVariant, variant
             )
                 .sortedWith(IssueSorter(filter.sortPrimary, filter.sortSecondary, filter.sortTertiary))
-                .filter(issueFilterFunction(filter, relationsMap, http.remoteUser ?: "<Anonymous>"))
+                .filter(issueFilterFunction(filter, relationsMap, http.user?.username ?: "<Anonymous>"))
 
             with(http) {
                 pageTitle = project.name
@@ -461,9 +461,8 @@
             ).applyFormData(http, dao, projectInfo.versions, projectInfo.variants)
 
             val openId = if (issue.id < 0) {
-                val remoteUser = http.remoteUser?.let { dao.findUserByName(it) }
                 val id = dao.insertIssue(issue)
-                dao.insertHistoryEvent(remoteUser, issue, id)
+                dao.insertHistoryEvent(http.user, issue, id)
                 id
             } else {
                 val reference = dao.findIssue(issue.id)
--- a/src/main/resources/localization/strings.properties	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/resources/localization/strings.properties	Sat May 17 17:39:48 2025 +0200
@@ -32,6 +32,7 @@
 button.comment=Comment
 button.component.create=New Component
 button.component.edit=Edit Component
+button.dismiss=Dismiss
 button.issue.all=All Issues
 button.issue.create.another=Create another Issue
 button.issue.create=New Issue
@@ -45,6 +46,7 @@
 button.variant.create=New Variant
 button.version.create=New Version
 button.version.edit=Edit Version
+button.whats-new=Show Changelog
 commit.redirect-link=If redirection does not work, click the following link:
 commit.success=Operation successful - you will be redirected in a second.
 component.active=Active
@@ -193,6 +195,7 @@
 version.status.Unreleased=Unreleased
 version.status=Status
 version=Version
+whats-new.info = A new version of LightPIT has been released. Do you want to check the release notes?
 issue.relations.type.DefectOf=defect of
 issue.relations.type.DefectOf.rev=defect
 issue.filter.sort.primary=Order by
--- a/src/main/resources/localization/strings_de.properties	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/resources/localization/strings_de.properties	Sat May 17 17:39:48 2025 +0200
@@ -32,6 +32,7 @@
 button.comment=Kommentieren
 button.component.create=Neue Komponente
 button.component.edit=Komponente Bearbeiten
+button.dismiss=Nicht Jetzt
 button.issue.all=Alle Vorg\u00e4nge
 button.issue.create.another=Weiteren Vorgang erstellen
 button.issue.create=Neuer Vorgang
@@ -45,6 +46,7 @@
 button.variant.create=Neue Variante
 button.version.create=Neue Version
 button.version.edit=Version Bearbeiten
+button.whats-new=Versionshinweise \u00d6ffnen
 commit.redirect-link=Falls die Weiterleitung nicht klappt, klicken Sie bitte hier:
 commit.success=Operation erfolgreich - Sie werden jeden Moment weitergeleitet.
 component.active=Aktiv
@@ -193,6 +195,7 @@
 version.status.Unreleased=Unver\u00f6ffentlicht
 version.status=Status
 version=Version
+whats-new.info = Eine neue LightPIT-Version wurde ver\u00f6ffentlicht. Wollen Sie mehr erfahren?
 issue.relations.type.DefectOf.rev=Fehler
 issue.relations.type.DefectOf=Fehler von
 issue.filter.sort.primary=Sortiere nach
--- a/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog-de.jspf	Sat May 17 17:39:48 2025 +0200
@@ -24,6 +24,12 @@
   --%>
 <%@ page contentType="text/html;charset=UTF-8" %>
 
+<h3>Version 1.6.0 (Vorschau)</h3>
+
+<ul>
+    <li>Pop-Up hinzugefügt, das über eine neue LightPIT-Version informiert.</li>
+</ul>
+
 <h3>Version 1.5.1</h3>
 
 <ul>
--- a/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/webapp/WEB-INF/changelogs/changelog.jspf	Sat May 17 17:39:48 2025 +0200
@@ -24,6 +24,12 @@
   --%>
 <%@ page contentType="text/html;charset=UTF-8" %>
 
+<h3>Version 1.6.0 (preview)</h3>
+
+<ul>
+    <li>Add popup informing about a new LightPIT release.</li>
+</ul>
+
 <h3>Version 1.5.1</h3>
 
 <ul>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Sat May 17 17:39:48 2025 +0200
@@ -31,7 +31,7 @@
 <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
 
 <%-- Version suffix for forcing browsers to update the CSS / JS files --%>
-<c:set scope="page" var="versionSuffix" value="20250313"/>
+<c:set scope="page" var="versionSuffix" value="20250517"/>
 
 <%-- Make the base href easily available at request scope --%>
 <c:set scope="page" var="baseHref" value="${requestScope[Constants.REQ_ATTR_BASE_HREF]}"/>
@@ -87,6 +87,16 @@
     </c:if>
     <script>
         const baseHref='${baseHref}';
+        <c:if test="${showWhatsNew}">
+        function closeWhatsNew(showMore) {
+            if (showMore) {
+                location.assign(baseHref + 'about');
+            } else {
+                document.getElementById('whats-new').style.display = 'none';
+                document.getElementById('page-area').classList.remove('blurred');
+            }
+        }
+        </c:if>
     </script>
     <script src="issue-search.js?v=${versionSuffix}" type="text/javascript"></script>
     <c:if test="${not empty javascriptFile}">
@@ -94,7 +104,7 @@
     </c:if>
 </head>
 <body>
-    <div id="page-area">
+    <div id="page-area" <c:if test="${showWhatsNew}">class="blurred"</c:if> >
         <div id="mainMenu">
             <div class="menuEntry"
                  <c:if test="${fn:startsWith(requestPath, '/projects/')}">data-active</c:if> >
@@ -143,5 +153,16 @@
             </div>
         </div>
     </div>
+    <c:if test="${showWhatsNew}">
+    <div id="whats-new">
+        <div>
+            <fmt:message key="whats-new.info"/>
+        </div>
+        <div class="hright bigskip">
+            <a class="button" onclick="closeWhatsNew(false)"><fmt:message key="button.dismiss"/></a>
+            <a class="button submit" onclick="closeWhatsNew(true)"><fmt:message key="button.whats-new"/></a>
+        </div>
+    </div>
+    </c:if>
 </body>
 </html>
--- a/src/main/webapp/lightpit.css	Fri Mar 14 08:09:05 2025 +0100
+++ b/src/main/webapp/lightpit.css	Sat May 17 17:39:48 2025 +0200
@@ -156,6 +156,21 @@
     padding: 1.5em;
 }
 
+#whats-new {
+    background: ghostwhite;
+    position: fixed;
+    top: 10%;
+    left: 50%;
+    transform: translate(-50%, 0);
+    padding: 1.5em;
+    border: #1c204e solid 2px;
+    border-radius: 4px;
+}
+
+.blurred {
+    filter: blur(5px);
+}
+
 button, a.button {
     display: inline-block;
     font-size: medium;
@@ -277,6 +292,10 @@
     margin-top: .5em;
 }
 
+.bigskip {
+    margin-top: 1.5em;
+}
+
 .info-box, .error-box, .warn-box {
     margin: 1.5em;
     border-style: dashed;

mercurial