Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func Route(
authenticatedAPI.Use(StoreMiddleware, JSONMiddleware, authentication)

authenticatedAPI.Path("/info").HandlerFunc(systemInfoController.GetSystemInfo).Methods("GET", "HEAD")
authenticatedAPI.Path("/info/seen_intro").HandlerFunc(systemInfoController.SetSeenIntro).Methods("POST")

authenticatedAPI.Path("/subscription").HandlerFunc(subscriptionController.Activate).Methods("POST")
authenticatedAPI.Path("/subscription").HandlerFunc(subscriptionController.GetSubscription).Methods("GET")
Expand Down
38 changes: 38 additions & 0 deletions api/system_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"errors"
"fmt"
"net/http"

"github.com/semaphoreui/semaphore/api/helpers"
Expand Down Expand Up @@ -76,6 +77,17 @@ func (c *SystemInfoController) GetSystemInfo(w http.ResponseWriter, r *http.Requ
plan = token.Plan
}

// Check if the user has seen the intro
seenIntroKey := fmt.Sprintf("seen_intro_%d", user.ID)
seenIntroVersion, err := helpers.Store(r).GetOption(seenIntroKey)
seenIntro := seenIntroVersion == util.Ver
Comment on lines +80 to +83
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'seenIntro' variable is calculated before checking if 'GetOption' returned an error. If an error occurs, 'seenIntroVersion' may be an empty string or other unexpected value, leading to incorrect comparison with 'util.Ver'. The calculation should happen after the error check, or a default value should be used on error.

Copilot uses AI. Check for mistakes.

if err != nil {
log.WithError(err).Error("Failed to get seen_intro option")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
Comment on lines +85 to +89
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling returns an HTTP error but doesn't set 'seenIntro' to a default value. This means the response body construction on line 91-103 will fail due to an undefined variable. Either return early (which is already done) or set a default value before the error check.

Copilot uses AI. Check for mistakes.

body := map[string]any{
"version": util.Version(),
"ansible": util.AnsibleVersion(),
Expand All @@ -87,7 +99,33 @@ func (c *SystemInfoController) GetSystemInfo(w http.ResponseWriter, r *http.Requ
"schedule_timezone": timezone,
"teams": util.Config.Teams,
"roles": roles,
"seen_intro": seenIntro,
}

helpers.WriteJSON(w, http.StatusOK, body)
}

func (c *SystemInfoController) SetSeenIntro(w http.ResponseWriter, r *http.Request) {
user := helpers.GetFromContext(r, "user").(*db.User)

var reqBody struct {
Version string `json:"version"`
}

if !helpers.Bind(w, r, &reqBody) {
return
}

seenIntroKey := fmt.Sprintf("seen_intro_%d", user.ID)

err := helpers.Store(r).SetOption(seenIntroKey, reqBody.Version)
if err != nil {
log.WithError(err).Error("Failed to set seen_intro option")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

helpers.WriteJSON(w, http.StatusOK, map[string]string{
"status": "ok",
})
}
7 changes: 7 additions & 0 deletions web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<template>
<v-app v-if="state === 'success'" class="app">
<IntroDialog v-model="introDialog" />

<YesNoDialog
:title="$t('projectRestoreResult')"
v-model="restoreProjectResultDialog"
Expand Down Expand Up @@ -853,6 +855,7 @@ import RestoreProjectForm from '@/components/RestoreProjectForm.vue';
import YesNoDialog from '@/components/YesNoDialog.vue';
import TaskLogDialog from '@/components/TaskLogDialog.vue';
import delay from '@/lib/delay';
import IntroDialog from '@/components/IntroDialog.vue';

const PROJECT_COLORS = ['red', 'blue', 'orange', 'green'];

Expand Down Expand Up @@ -924,6 +927,7 @@ function getSystemLang() {
export default {
name: 'App',
components: {
IntroDialog,
SubscriptionForm,
TaskLogDialog,
YesNoDialog,
Expand Down Expand Up @@ -970,6 +974,8 @@ export default {
...LANGUAGES[lang],
})),
],

introDialog: null,
};
},

Expand Down Expand Up @@ -1060,6 +1066,7 @@ export default {
try {
await this.loadData();
this.state = 'success';
this.introDialog = !this.systemInfo.seen_intro;
} catch (err) {
if (err.response && err.response.status === 401) {
if (this.$route.path !== '/auth/login') {
Expand Down
Binary file added web/src/assets/intro/poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions web/src/components/IntroDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<template>
<EditDialog
v-model="dialog"
:max-width="1000"
hide-buttons
:expandable="true"
no-body-paddings
@close="onClose()"
test-id="introDialog"
title="New release v2.17 🎉"
>
<template v-slot:form="{}">
<v-carousel>
<v-carousel-item>

</v-carousel-item>
<v-carousel-item> </v-carousel-item>
Comment on lines +13 to +17
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The carousel component is empty with no content. Users will see a blank dialog when the intro is displayed. The carousel items should contain actual content like release notes, feature highlights, or other introductory information.

Suggested change
<v-carousel>
<v-carousel-item>
</v-carousel-item>
<v-carousel-item> </v-carousel-item>
<v-carousel
cycle
hide-delimiter-background
>
<v-carousel-item>
<v-container fluid>
<v-row>
<v-col cols="12">
<h2 class="text-h5 mb-4">What&apos;s new in v2.17</h2>
</v-col>
<v-col cols="12">
<ul class="mb-2">
<li>Improved job log viewer for large outputs.</li>
<li>Faster project dashboard loading times.</li>
<li>Enhanced secrets management and permissions.</li>
</ul>
<p class="mt-2 mb-0">
Explore these improvements in your existing projects, or create a new project to try them out.
</p>
</v-col>
</v-row>
</v-container>
</v-carousel-item>
<v-carousel-item>
<v-container fluid>
<v-row>
<v-col cols="12">
<h2 class="text-h5 mb-4">Getting started &amp; tips</h2>
</v-col>
<v-col cols="12">
<ul class="mb-2">
<li>Review your project settings to configure notifications and access control.</li>
<li>Use templates to standardize common jobs across your team.</li>
<li>Check the documentation for detailed upgrade notes for v2.17.</li>
</ul>
<p class="mt-2 mb-0">
You can reopen this dialog from the help menu if you want to review these notes later.
</p>
</v-col>
</v-row>
</v-container>
</v-carousel-item>

Copilot uses AI. Check for mistakes.
</v-carousel>
</template>
</EditDialog>
</template>
<script>
import EditDialog from '@/components/EditDialog.vue';
import ProjectMixin from '@/components/ProjectMixin';
export default {
components: { EditDialog },
mixins: [ProjectMixin],
props: {
value: Boolean,
projectId: Number,
itemId: Number,
systemInfo: Object,
Comment on lines +33 to +35
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component accepts 'projectId', 'itemId', 'template', and 'systemInfo' props that are never used. These props appear to be copied from a template or another component and should be removed to avoid confusion.

Suggested change
projectId: Number,
itemId: Number,
systemInfo: Object,

Copilot uses AI. Check for mistakes.
},
watch: {
async dialog(val) {
this.$emit('input', val);
},
async value(val) {
this.item = null;
this.template = null;
Comment on lines +44 to +45
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The watcher sets 'this.item' and 'this.template' to null, but these properties don't exist in the component's data object. This suggests incomplete refactoring from a template component.

Copilot uses AI. Check for mistakes.
this.dialog = val;
await this.loadData();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'loadData' method is called but never implemented. This will attempt to call an undefined method when the dialog value changes, potentially causing runtime errors.

Copilot uses AI. Check for mistakes.
},
},
data() {
return {
dialog: null,
model: 0,
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'model' data property is defined but never used in the component. This should be removed to keep the code clean.

Suggested change
model: 0,

Copilot uses AI. Check for mistakes.
};
},
methods: {
close() {
this.dialog = false;
this.onClose();
},
async loadData() {
//
},
onClose() {
this.$emit('close');
},
Comment on lines +68 to +70
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no API call to persist that the user has seen the intro dialog. When the dialog is closed, the backend should be notified via the '/info/seen_intro' endpoint so the dialog doesn't show again on the next login.

Copilot uses AI. Check for mistakes.
},
};
</script>
Loading