diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index 74ccc1890f..4b4a0cd193 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -2972,9 +2972,12 @@ CREATE TABLE workspace_builds (
template_version_preset_id uuid,
has_ai_task boolean,
has_external_agent boolean,
+ notified_autostop_deadline timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
CONSTRAINT workspace_builds_deadline_below_max_deadline CHECK ((((deadline <> '0001-01-01 00:00:00+00'::timestamp with time zone) AND (deadline <= max_deadline)) OR (max_deadline = '0001-01-01 00:00:00+00'::timestamp with time zone)))
);
+COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.';
+
CREATE TABLE workspaces (
id uuid NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -3347,7 +3350,8 @@ CREATE TABLE templates (
max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL,
use_classic_parameter_flow boolean DEFAULT false NOT NULL,
cors_behavior cors_behavior DEFAULT 'simple'::cors_behavior NOT NULL,
- disable_module_cache boolean DEFAULT false NOT NULL
+ disable_module_cache boolean DEFAULT false NOT NULL,
+ time_til_autostop_notify bigint DEFAULT 0 NOT NULL
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
@@ -3370,6 +3374,8 @@ COMMENT ON COLUMN templates.deprecated IS 'If set to a non empty string, the tem
COMMENT ON COLUMN templates.use_classic_parameter_flow IS 'Determines whether to default to the dynamic parameter creation flow for this template or continue using the legacy classic parameter creation flow.This is a template wide setting, the template admin can revert to the classic flow if there are any issues. An escape hatch is required, as workspace creation is a core workflow and cannot break. This column will be removed when the dynamic parameter creation flow is stable.';
+COMMENT ON COLUMN templates.time_til_autostop_notify IS 'How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.';
+
CREATE VIEW template_with_names AS
SELECT templates.id,
templates.created_at,
@@ -3402,6 +3408,7 @@ CREATE VIEW template_with_names AS
templates.use_classic_parameter_flow,
templates.cors_behavior,
templates.disable_module_cache,
+ templates.time_til_autostop_notify,
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS created_by_username,
COALESCE(visible_users.name, ''::text) AS created_by_name,
@@ -3858,6 +3865,7 @@ CREATE VIEW workspace_build_with_user AS
workspace_builds.template_version_preset_id,
workspace_builds.has_ai_task,
workspace_builds.has_external_agent,
+ workspace_builds.notified_autostop_deadline,
COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url,
COALESCE(visible_users.username, ''::text) AS initiator_by_username,
COALESCE(visible_users.name, ''::text) AS initiator_by_name
diff --git a/coderd/database/migrations/000528_workspace_autostop_notification.down.sql b/coderd/database/migrations/000528_workspace_autostop_notification.down.sql
new file mode 100644
index 0000000000..96fd3b6fcb
--- /dev/null
+++ b/coderd/database/migrations/000528_workspace_autostop_notification.down.sql
@@ -0,0 +1,51 @@
+DELETE FROM notification_templates WHERE id = '6f6cb984-c167-4fa5-bb87-1058dd642779';
+
+DROP VIEW workspace_build_with_user;
+
+ALTER TABLE workspace_builds DROP COLUMN notified_autostop_deadline;
+
+CREATE VIEW workspace_build_with_user AS
+SELECT
+ workspace_builds.id,
+ workspace_builds.created_at,
+ workspace_builds.updated_at,
+ workspace_builds.workspace_id,
+ workspace_builds.template_version_id,
+ workspace_builds.build_number,
+ workspace_builds.transition,
+ workspace_builds.initiator_id,
+ workspace_builds.job_id,
+ workspace_builds.deadline,
+ workspace_builds.reason,
+ workspace_builds.daily_cost,
+ workspace_builds.max_deadline,
+ workspace_builds.template_version_preset_id,
+ workspace_builds.has_ai_task,
+ workspace_builds.has_external_agent,
+ COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url,
+ COALESCE(visible_users.username, ''::text) AS initiator_by_username,
+ COALESCE(visible_users.name, ''::text) AS initiator_by_name
+FROM
+ workspace_builds
+LEFT JOIN
+ visible_users ON workspace_builds.initiator_id = visible_users.id;
+
+COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
+
+DROP VIEW template_with_names;
+
+ALTER TABLE templates DROP COLUMN time_til_autostop_notify;
+
+CREATE VIEW template_with_names AS
+SELECT templates.*,
+ COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
+ COALESCE(visible_users.username, ''::text) AS created_by_username,
+ COALESCE(visible_users.name, ''::text) AS created_by_name,
+ COALESCE(organizations.name, ''::text) AS organization_name,
+ COALESCE(organizations.display_name, ''::text) AS organization_display_name,
+ COALESCE(organizations.icon, ''::text) AS organization_icon
+FROM ((templates
+ LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
+ LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
+
+COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
diff --git a/coderd/database/migrations/000528_workspace_autostop_notification.up.sql b/coderd/database/migrations/000528_workspace_autostop_notification.up.sql
new file mode 100644
index 0000000000..9d7ed1ef55
--- /dev/null
+++ b/coderd/database/migrations/000528_workspace_autostop_notification.up.sql
@@ -0,0 +1,68 @@
+ALTER TABLE templates ADD COLUMN time_til_autostop_notify bigint DEFAULT 0 NOT NULL;
+
+COMMENT ON COLUMN templates.time_til_autostop_notify IS 'How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.';
+
+DROP VIEW template_with_names;
+
+CREATE VIEW template_with_names AS
+SELECT templates.*,
+ COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
+ COALESCE(visible_users.username, ''::text) AS created_by_username,
+ COALESCE(visible_users.name, ''::text) AS created_by_name,
+ COALESCE(organizations.name, ''::text) AS organization_name,
+ COALESCE(organizations.display_name, ''::text) AS organization_display_name,
+ COALESCE(organizations.icon, ''::text) AS organization_icon
+FROM ((templates
+ LEFT JOIN visible_users ON ((templates.created_by = visible_users.id)))
+ LEFT JOIN organizations ON ((templates.organization_id = organizations.id)));
+
+COMMENT ON VIEW template_with_names IS 'Joins in the display name information such as username, avatar, and organization name.';
+
+ALTER TABLE workspace_builds ADD COLUMN notified_autostop_deadline timestamptz DEFAULT '0001-01-01 00:00:00+00' NOT NULL;
+
+COMMENT ON COLUMN workspace_builds.notified_autostop_deadline IS 'The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.';
+
+DROP VIEW workspace_build_with_user;
+
+CREATE VIEW workspace_build_with_user AS
+SELECT
+ workspace_builds.id,
+ workspace_builds.created_at,
+ workspace_builds.updated_at,
+ workspace_builds.workspace_id,
+ workspace_builds.template_version_id,
+ workspace_builds.build_number,
+ workspace_builds.transition,
+ workspace_builds.initiator_id,
+ workspace_builds.job_id,
+ workspace_builds.deadline,
+ workspace_builds.reason,
+ workspace_builds.daily_cost,
+ workspace_builds.max_deadline,
+ workspace_builds.template_version_preset_id,
+ workspace_builds.has_ai_task,
+ workspace_builds.has_external_agent,
+ workspace_builds.notified_autostop_deadline,
+ COALESCE(visible_users.avatar_url, ''::text) AS initiator_by_avatar_url,
+ COALESCE(visible_users.username, ''::text) AS initiator_by_username,
+ COALESCE(visible_users.name, ''::text) AS initiator_by_name
+FROM
+ workspace_builds
+LEFT JOIN
+ visible_users ON workspace_builds.initiator_id = visible_users.id;
+
+COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
+
+INSERT INTO notification_templates (
+ id, name, title_template, body_template, actions, "group", method, kind, enabled_by_default
+) VALUES (
+ '6f6cb984-c167-4fa5-bb87-1058dd642779',
+ 'Workspace Autostop Reminder',
+ E'Your workspace "{{.Labels.workspace}}" will stop soon',
+ E'Your workspace **{{.Labels.workspace}}** is scheduled to automatically stop at {{.Labels.deadline}}.\n\nConnect to it or extend the deadline to keep it running.',
+ '[{"label": "View workspace", "url": "{{base_url}}/@{{.UserUsername}}/{{.Labels.workspace}}"}]'::jsonb,
+ 'Workspace Events',
+ NULL,
+ 'system'::notification_template_kind,
+ true
+);
diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go
index 0810ef21b1..0835527b5d 100644
--- a/coderd/database/modelqueries.go
+++ b/coderd/database/modelqueries.go
@@ -129,6 +129,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
+ &i.TimeTilAutostopNotify,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 1f2965caa4..89ad03a33d 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -5706,6 +5706,7 @@ type Template struct {
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
+ TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"`
CreatedByAvatarURL string `db:"created_by_avatar_url" json:"created_by_avatar_url"`
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
CreatedByName string `db:"created_by_name" json:"created_by_name"`
@@ -5756,6 +5757,8 @@ type TemplateTable struct {
UseClassicParameterFlow bool `db:"use_classic_parameter_flow" json:"use_classic_parameter_flow"`
CorsBehavior CorsBehavior `db:"cors_behavior" json:"cors_behavior"`
DisableModuleCache bool `db:"disable_module_cache" json:"disable_module_cache"`
+ // How long before the workspace autostop deadline to send a reminder notification, in nanoseconds. 0 disables the notification.
+ TimeTilAutostopNotify int64 `db:"time_til_autostop_notify" json:"time_til_autostop_notify"`
}
// Records aggregated usage statistics for templates/users. All usage is rounded up to the nearest minute.
@@ -6413,25 +6416,26 @@ type WorkspaceAppStatus struct {
// Joins in the username + avatar url of the initiated by user.
type WorkspaceBuild struct {
- ID uuid.UUID `db:"id" json:"id"`
- CreatedAt time.Time `db:"created_at" json:"created_at"`
- UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
- WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
- TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
- BuildNumber int32 `db:"build_number" json:"build_number"`
- Transition WorkspaceTransition `db:"transition" json:"transition"`
- InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
- JobID uuid.UUID `db:"job_id" json:"job_id"`
- Deadline time.Time `db:"deadline" json:"deadline"`
- Reason BuildReason `db:"reason" json:"reason"`
- DailyCost int32 `db:"daily_cost" json:"daily_cost"`
- MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
- TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
- HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
- HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
- InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"`
- InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"`
- InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"`
+ ID uuid.UUID `db:"id" json:"id"`
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
+ WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
+ TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
+ BuildNumber int32 `db:"build_number" json:"build_number"`
+ Transition WorkspaceTransition `db:"transition" json:"transition"`
+ InitiatorID uuid.UUID `db:"initiator_id" json:"initiator_id"`
+ JobID uuid.UUID `db:"job_id" json:"job_id"`
+ Deadline time.Time `db:"deadline" json:"deadline"`
+ Reason BuildReason `db:"reason" json:"reason"`
+ DailyCost int32 `db:"daily_cost" json:"daily_cost"`
+ MaxDeadline time.Time `db:"max_deadline" json:"max_deadline"`
+ TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
+ HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
+ HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
+ NotifiedAutostopDeadline time.Time `db:"notified_autostop_deadline" json:"notified_autostop_deadline"`
+ InitiatorByAvatarUrl string `db:"initiator_by_avatar_url" json:"initiator_by_avatar_url"`
+ InitiatorByUsername string `db:"initiator_by_username" json:"initiator_by_username"`
+ InitiatorByName string `db:"initiator_by_name" json:"initiator_by_name"`
}
type WorkspaceBuildParameter struct {
@@ -6460,6 +6464,8 @@ type WorkspaceBuildTable struct {
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
HasAITask sql.NullBool `db:"has_ai_task" json:"has_ai_task"`
HasExternalAgent sql.NullBool `db:"has_external_agent" json:"has_external_agent"`
+ // The autostop deadline value that an autostop reminder notification was last sent for. Used for idempotence: when it equals the build deadline the reminder has already been sent, and it re-arms automatically when the deadline changes.
+ NotifiedAutostopDeadline time.Time `db:"notified_autostop_deadline" json:"notified_autostop_deadline"`
}
type WorkspaceLatestBuild struct {
diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go
index f8e6589576..fdf4425493 100644
--- a/coderd/database/queries.sql.go
+++ b/coderd/database/queries.sql.go
@@ -26425,7 +26425,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, templateID
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names
WHERE
@@ -26469,6 +26469,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
+ &i.TimeTilAutostopNotify,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -26481,7 +26482,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon
FROM
template_with_names AS templates
WHERE
@@ -26533,6 +26534,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
+ &i.TimeTilAutostopNotify,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -26544,7 +26546,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
}
const getTemplates = `-- name: GetTemplates :many
-SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
+SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify, created_by_avatar_url, created_by_username, created_by_name, organization_name, organization_display_name, organization_icon FROM template_with_names AS templates
ORDER BY (name, id) ASC
`
@@ -26589,6 +26591,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
+ &i.TimeTilAutostopNotify,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -26611,7 +26614,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
- t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
+ t.id, t.created_at, t.updated_at, t.organization_id, t.deleted, t.name, t.provisioner, t.active_version_id, t.description, t.default_ttl, t.created_by, t.icon, t.user_acl, t.group_acl, t.display_name, t.allow_user_cancel_workspace_jobs, t.allow_user_autostart, t.allow_user_autostop, t.failure_ttl, t.time_til_dormant, t.time_til_dormant_autodelete, t.autostop_requirement_days_of_week, t.autostop_requirement_weeks, t.autostart_block_days_of_week, t.require_active_version, t.deprecated, t.activity_bump, t.max_port_sharing_level, t.use_classic_parameter_flow, t.cors_behavior, t.disable_module_cache, t.time_til_autostop_notify, t.created_by_avatar_url, t.created_by_username, t.created_by_name, t.organization_name, t.organization_display_name, t.organization_icon
FROM
template_with_names AS t
LEFT JOIN
@@ -26771,6 +26774,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.UseClassicParameterFlow,
&i.CorsBehavior,
&i.DisableModuleCache,
+ &i.TimeTilAutostopNotify,
&i.CreatedByAvatarURL,
&i.CreatedByUsername,
&i.CreatedByName,
@@ -32166,7 +32170,7 @@ const getAuthenticatedWorkspaceAgentAndBuildByAuthToken = `-- name: GetAuthentic
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl,
workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted,
- workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
+ workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.has_external_agent, workspace_build_with_user.notified_autostop_deadline, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name,
tasks.id AS task_id
FROM
workspace_agents
@@ -32312,6 +32316,7 @@ func (q *sqlQuerier) GetAuthenticatedWorkspaceAgentAndBuildByAuthToken(ctx conte
&i.WorkspaceBuild.TemplateVersionPresetID,
&i.WorkspaceBuild.HasAITask,
&i.WorkspaceBuild.HasExternalAgent,
+ &i.WorkspaceBuild.NotifiedAutostopDeadline,
&i.WorkspaceBuild.InitiatorByAvatarUrl,
&i.WorkspaceBuild.InitiatorByUsername,
&i.WorkspaceBuild.InitiatorByName,
@@ -35449,7 +35454,7 @@ func (q *sqlQuerier) InsertWorkspaceBuildParameters(ctx context.Context, arg Ins
}
const getActiveWorkspaceBuildsByTemplateID = `-- name: GetActiveWorkspaceBuildsByTemplateID :many
-SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
+SELECT wb.id, wb.created_at, wb.updated_at, wb.workspace_id, wb.template_version_id, wb.build_number, wb.transition, wb.initiator_id, wb.job_id, wb.deadline, wb.reason, wb.daily_cost, wb.max_deadline, wb.template_version_preset_id, wb.has_ai_task, wb.has_external_agent, wb.notified_autostop_deadline, wb.initiator_by_avatar_url, wb.initiator_by_username, wb.initiator_by_name
FROM (
SELECT
workspace_id, MAX(build_number) as max_build_number
@@ -35505,6 +35510,7 @@ func (q *sqlQuerier) GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, t
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -35604,7 +35610,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a
const getLatestWorkspaceBuildByWorkspaceID = `-- name: GetLatestWorkspaceBuildByWorkspaceID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -35635,6 +35641,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, w
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -35700,7 +35707,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildWithStatusByWorkspaceID(ctx context.
const getLatestWorkspaceBuildsByWorkspaceIDs = `-- name: GetLatestWorkspaceBuildsByWorkspaceIDs :many
SELECT
DISTINCT ON (workspace_id)
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -35735,6 +35742,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -35754,7 +35762,7 @@ func (q *sqlQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context,
const getWorkspaceBuildByID = `-- name: GetWorkspaceBuildByID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -35783,6 +35791,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -35792,7 +35801,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (W
const getWorkspaceBuildByJobID = `-- name: GetWorkspaceBuildByJobID :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -35821,6 +35830,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -35830,7 +35840,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UU
const getWorkspaceBuildByWorkspaceIDAndBuildNumber = `-- name: GetWorkspaceBuildByWorkspaceIDAndBuildNumber :one
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -35863,6 +35873,7 @@ func (q *sqlQuerier) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Co
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -36037,7 +36048,7 @@ func (q *sqlQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, sinc
const getWorkspaceBuildsByWorkspaceID = `-- name: GetWorkspaceBuildsByWorkspaceID :many
SELECT
- id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name
+ id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name
FROM
workspace_build_with_user AS workspace_builds
WHERE
@@ -36109,6 +36120,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -36127,7 +36139,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg Ge
}
const getWorkspaceBuildsCreatedAfter = `-- name: GetWorkspaceBuildsCreatedAfter :many
-SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
+SELECT id, created_at, updated_at, workspace_id, template_version_id, build_number, transition, initiator_id, job_id, deadline, reason, daily_cost, max_deadline, template_version_preset_id, has_ai_task, has_external_agent, notified_autostop_deadline, initiator_by_avatar_url, initiator_by_username, initiator_by_name FROM workspace_build_with_user WHERE created_at > $1
`
func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) {
@@ -36156,6 +36168,7 @@ func (q *sqlQuerier) GetWorkspaceBuildsCreatedAfter(ctx context.Context, created
&i.TemplateVersionPresetID,
&i.HasAITask,
&i.HasExternalAgent,
+ &i.NotifiedAutostopDeadline,
&i.InitiatorByAvatarUrl,
&i.InitiatorByUsername,
&i.InitiatorByName,
@@ -37579,7 +37592,7 @@ LEFT JOIN LATERAL (
) latest_build ON TRUE
LEFT JOIN LATERAL (
SELECT
- id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache
+ id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, deprecated, activity_bump, max_port_sharing_level, use_classic_parameter_flow, cors_behavior, disable_module_cache, time_til_autostop_notify
FROM
templates
WHERE
diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md
index a41fc4c9a5..62b0302dd1 100644
--- a/docs/admin/security/audit-logs.md
+++ b/docs/admin/security/audit-logs.md
@@ -40,12 +40,12 @@ We track the following resources:
| PrebuildsSettings
|
| Field | Tracked |
| | id | false |
| reconciliation_paused | true |
|
| RoleSyncSettings
| | Field | Tracked |
| | field | true |
| mapping | true |
|
| TaskTable
| | Field | Tracked |
| | created_at | false |
| deleted_at | false |
| display_name | true |
| id | true |
| name | true |
| organization_id | false |
| owner_id | true |
| prompt | true |
| template_parameters | true |
| template_version_id | true |
| workspace_id | true |
|
-| Template
write, delete | | Field | Tracked |
| | active_version_id | true |
| activity_bump | true |
| allow_user_autostart | true |
| allow_user_autostop | true |
| allow_user_cancel_workspace_jobs | true |
| autostart_block_days_of_week | true |
| autostop_requirement_days_of_week | true |
| autostop_requirement_weeks | true |
| cors_behavior | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| default_ttl | true |
| deleted | false |
| deprecated | true |
| description | true |
| disable_module_cache | true |
| display_name | true |
| failure_ttl | true |
| group_acl | true |
| icon | true |
| id | true |
| max_port_sharing_level | true |
| name | true |
| organization_display_name | false |
| organization_icon | false |
| organization_id | false |
| organization_name | false |
| provisioner | true |
| require_active_version | true |
| time_til_dormant | true |
| time_til_dormant_autodelete | true |
| updated_at | false |
| use_classic_parameter_flow | true |
| user_acl | true |
|
+| Template
write, delete | | Field | Tracked |
| | active_version_id | true |
| activity_bump | true |
| allow_user_autostart | true |
| allow_user_autostop | true |
| allow_user_cancel_workspace_jobs | true |
| autostart_block_days_of_week | true |
| autostop_requirement_days_of_week | true |
| autostop_requirement_weeks | true |
| cors_behavior | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| default_ttl | true |
| deleted | false |
| deprecated | true |
| description | true |
| disable_module_cache | true |
| display_name | true |
| failure_ttl | true |
| group_acl | true |
| icon | true |
| id | true |
| max_port_sharing_level | true |
| name | true |
| organization_display_name | false |
| organization_icon | false |
| organization_id | false |
| organization_name | false |
| provisioner | true |
| require_active_version | true |
| time_til_autostop_notify | true |
| time_til_dormant | true |
| time_til_dormant_autodelete | true |
| updated_at | false |
| use_classic_parameter_flow | true |
| user_acl | true |
|
| TemplateVersion
create, write | | Field | Tracked |
| | archived | true |
| created_at | false |
| created_by | true |
| created_by_avatar_url | false |
| created_by_name | false |
| created_by_username | false |
| external_auth_providers | false |
| has_ai_task | false |
| has_external_agent | false |
| id | true |
| job_id | false |
| message | false |
| name | true |
| organization_id | false |
| readme | true |
| source_example_id | false |
| template_id | true |
| updated_at | false |
|
| User
create, write, delete | | Field | Tracked |
| | avatar_url | false |
| chat_spend_limit_micros | true |
| created_at | false |
| deleted | true |
| email | true |
| github_com_user_id | false |
| hashed_one_time_passcode | false |
| hashed_password | true |
| id | true |
| is_service_account | true |
| is_system | true |
| last_seen_at | false |
| login_type | true |
| name | true |
| one_time_passcode_expires_at | true |
| quiet_hours_schedule | true |
| rbac_roles | true |
| status | true |
| updated_at | false |
| username | true |
|
| UserSecret
create, write, delete | | Field | Tracked |
| | created_at | false |
| description | true |
| env_name | true |
| file_path | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
| value | true |
| value_key_id | false |
|
| UserSkill
create, write, delete | | Field | Tracked |
| | content | true |
| created_at | false |
| description | true |
| id | true |
| name | true |
| updated_at | false |
| user_id | true |
|
-| WorkspaceBuild
start, stop | | Field | Tracked |
| | build_number | false |
| created_at | false |
| daily_cost | false |
| deadline | false |
| has_ai_task | false |
| has_external_agent | false |
| id | false |
| initiator_by_avatar_url | false |
| initiator_by_name | false |
| initiator_by_username | false |
| initiator_id | false |
| job_id | false |
| max_deadline | false |
| reason | false |
| template_version_id | true |
| template_version_preset_id | false |
| transition | false |
| updated_at | false |
| workspace_id | false |
|
+| WorkspaceBuild
start, stop | | Field | Tracked |
| | build_number | false |
| created_at | false |
| daily_cost | false |
| deadline | false |
| has_ai_task | false |
| has_external_agent | false |
| id | false |
| initiator_by_avatar_url | false |
| initiator_by_name | false |
| initiator_by_username | false |
| initiator_id | false |
| job_id | false |
| max_deadline | false |
| notified_autostop_deadline | false |
| reason | false |
| template_version_id | true |
| template_version_preset_id | false |
| transition | false |
| updated_at | false |
| workspace_id | false |
|
| WorkspaceProxy
| | Field | Tracked |
| | created_at | true |
| deleted | false |
| derp_enabled | true |
| derp_only | true |
| display_name | true |
| icon | true |
| id | true |
| name | true |
| region_id | true |
| token_hashed_secret | true |
| updated_at | false |
| url | true |
| version | true |
| wildcard_hostname | true |
|
| WorkspaceTable
| | Field | Tracked |
| | automatic_updates | true |
| autostart_schedule | true |
| created_at | false |
| deleted | false |
| deleting_at | true |
| dormant_at | true |
| favorite | true |
| group_acl | true |
| id | true |
| last_used_at | false |
| name | true |
| next_start_at | true |
| organization_id | false |
| owner_id | true |
| template_id | true |
| ttl | true |
| updated_at | false |
| user_acl | true |
|
diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go
index d509581b63..f88c50b86e 100644
--- a/enterprise/audit/table.go
+++ b/enterprise/audit/table.go
@@ -130,6 +130,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"use_classic_parameter_flow": ActionTrack,
"cors_behavior": ActionTrack,
"disable_module_cache": ActionTrack,
+ "time_til_autostop_notify": ActionTrack,
},
&database.TemplateVersion{}: {
"id": ActionTrack,
@@ -213,6 +214,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"template_version_preset_id": ActionIgnore, // Never changes.
"has_ai_task": ActionIgnore, // Never changes.
"has_external_agent": ActionIgnore, // Never changes.
+ "notified_autostop_deadline": ActionIgnore, // Updated by the notification system, not by user action.
},
&database.AuditableGroup{}: {
"id": ActionTrack,