Skip to content

Qlty: Found 32 lines of similar code in 2 locations (mass = 88) #366

@CropWatchDevelopment

Description

@CropWatchDevelopment

Issue on Qlty

Found 77 lines of similar code in 2 locations (mass = 402)
scripts/generate-sample-pdf.cjs

const fs = require(
'fs
')
;

const path = require(
'path
')
;

const PDFDocument = require(
'pdfkit
')
;

(
async
() => {
try {
const outPath = path.join(
process.cwd
(),
'static/test-sample-report.pdf
')
;

const possibleFontPaths = [
  path.join(

process.cwd
(),
'static/fonts/NotoSansJP-Regular.ttf
'),
path.join(
process.cwd
(),
'static/fonts/NotoSansJP-Regular.otf
'),
Found 87 lines of similar code in 2 locations (mass = 402)
scripts/generate-sample-pdf.js

const
fs =
require
(
'fs'
);
const
path =
require
(
'path'
);
const
PDFDocument =
require
(
'pdfkit'
);
(
async
() => {

try
{

const
outPath = path.join(process.cwd(),
'static/test-sample-report.pdf'
);

const
possibleFontPaths = [
path.join(process.cwd(),
'static/fonts/NotoSansJP-Regular.ttf'
),
path.join(process.cwd(),
'static/fonts/NotoSansJP-Regular.otf'
),
Found 32 lines of similar code in 2 locations (mass = 71)
src/lib/interfaces/IAirDataService.ts

import

type
{ AirData, AirDataInsert, AirDataUpdate }
from

'../models/AirData'
;
/**

  • Service interface for air data operations
    */

export

interface
IAirDataService {

/**

  • Get air data by device EUI

@param
devEui The device EUI
*/
Found 32 lines of similar code in 2 locations (mass = 71)
src/lib/interfaces/ISoilDataService.ts

import

type
{ SoilData, SoilDataInsert }
from

'../models/SoilData'
;
/**

  • Service interface for soil data operations
    */

export

interface
ISoilDataService {

/**

  • Get soil data by device EUI

@param
devEui The device EUI
*/
Found 16 lines of similar code in 2 locations (mass = 64)
src/lib/models/AirData.ts

import

type
{ Database }
from

'../../../database.types'
;
/**

  • Represents air data from the database
    */

export

type
AirData = Database[
'public'
][
'Tables'
][
'cw_air_data'
][
'Row'
];
/**

  • Type for creating new air data
    */
    Found 17 lines of similar code in 2 locations (mass = 64)
    src/lib/models/User.ts

import

type
{ Database }
from

'../../../database.types'
;
/**

  • Represents a user profile from the database
    */

export

type
User = Database[
'public'
][
'Tables'
][
'profiles'
][
'Row'
];
/**

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'dev_eui'
, devEui)
.order(
'id'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding device owners by device EUI: ${devEui}
Found 16 lines of similar code in 6 locations (mass = 74)
src/lib/repositories/DeviceOwnersRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'user_id'
, userId)
.order(
'id'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding device owners by user ID: ${userId}
Found 18 lines of similar code in 2 locations (mass = 82)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'id'
, id)
.single();

if
(error) {

// For "no rows found" error, return null

if
(error.code ===
'PGRST116'
) {

return

null
;
Found 16 lines of similar code in 4 locations (mass = 78)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.ilike(
'manufacturer'
,
% ${manufacturer} %
)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding device types by manufacturer: ${manufacturer}
Found 13 lines of similar code in 4 locations (mass = 78)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.ilike(
'model'
,
% ${model} %
)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(error,
Error finding device types by model: ${model}
);
}
Found 16 lines of similar code in 6 locations (mass = 74)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'data_table_v2'
, dataTable)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding device types by data table: ${dataTable}
Found 16 lines of similar code in 4 locations (mass = 78)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.ilike(
'name'
,
% ${searchTerm} %
)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error searching device types by name: ${searchTerm}
Found 16 lines of similar code in 4 locations (mass = 78)
src/lib/repositories/DeviceTypeRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.ilike(
'primary_data_notation'
,
% ${dataType} %
)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding device types by primary data type: ${dataType}
Found 16 lines of similar code in 6 locations (mass = 74)
src/lib/repositories/LocationRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'owner_id'
, ownerId)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding locations by owner ID: ${ownerId}
Found 21 lines of similar code in 5 locations (mass = 85)
src/lib/repositories/ReportAlertPointRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'report_id'
, reportId)
.order(
'created_at'
, {
ascending
:
false
});

if
(error) {

throw
error;
}
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportAlertPointRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.insert(alertPoint)
.select()
.single();

if
(error) {

throw
error;
}
Found 19 lines of similar code in 3 locations (mass = 83)
src/lib/repositories/ReportAlertPointRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.update(updates)
.eq(
'id'
, id)
.select()
.single();

if
(error) {

throw
error;
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportAlertPointRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.upsert(alertPoint)
.select()
.single();

if
(error) {

throw
error;
}
Found 15 lines of similar code in 5 locations (mass = 67)
src/lib/repositories/ReportAlertPointRepository.ts

try
{

const
{ error } =
await

this
.supabase.from(
this
.tableName).delete().eq(
'report_id'
, reportId);

if
(error) {

throw
error;
}
}
catch
(error) {

this
.errorHandler.handleDatabaseError(
error
as

any
,

Error deleting alert points for report: ${reportId}
Found 21 lines of similar code in 5 locations (mass = 85)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'report_id'
, reportId)
.order(
'created_at'
, {
ascending
:
false
});

if
(error) {

throw
error;
}
Found 26 lines of similar code in 2 locations (mass = 72)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(

*, communication_methods:communication_method(*)

			)
			.eq(

'report_id'
, reportId)
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.insert(recipient)
.select()
.single();

if
(error) {

throw
error;
}
Found 19 lines of similar code in 3 locations (mass = 83)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.update(updates)
.eq(
'id'
, id)
.select()
.single();

if
(error) {

throw
error;
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.upsert(recipient)
.select()
.single();

if
(error) {

throw
error;
}
Found 15 lines of similar code in 5 locations (mass = 67)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ error } =
await

this
.supabase.from(
this
.tableName).delete().eq(
'report_id'
, reportId);

if
(error) {

throw
error;
}
}
catch
(error) {

this
.errorHandler.handleDatabaseError(
error
as

any
,

Error deleting recipients for report: ${reportId}
Found 12 lines of similar code in 5 locations (mass = 67)
src/lib/repositories/ReportRecipientRepository.ts

try
{

const
{ error } =
await

this
.supabase.from(
this
.tableName).delete().eq(
'id'
, id);

if
(error) {

throw
error;
}
}
catch
(error) {

this
.errorHandler.handleDatabaseError(error
as

any
,
Error deleting recipient: ${id}
);

throw
error;
}
Found 21 lines of similar code in 5 locations (mass = 85)
src/lib/repositories/ReportRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'dev_eui'
, devEui)
.order(
'created_at'
, {
ascending
:
false
});

if
(error) {

throw
error;
}
Found 29 lines of similar code in 2 locations (mass = 72)
src/lib/repositories/ReportRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(

`
*,
report_recipients(
,
communication_methods:communication_method(
)
)
Found 12 lines of similar code in 5 locations (mass = 67)
src/lib/repositories/ReportRepository.ts

try
{

const
{ error } =
await

this
.supabase.from(
this
.tableName).delete().eq(
'report_id'
, reportId);

if
(error) {

throw
error;
}
}
catch
(error) {

this
.errorHandler.handleDatabaseError(error
as

any
,
Error deleting report: ${reportId}
);

throw
error;
}
Found 21 lines of similar code in 5 locations (mass = 85)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'report_id'
, reportId)
.order(
'created_at'
, {
ascending
:
false
});

if
(error) {

throw
error;
}
Found 21 lines of similar code in 5 locations (mass = 85)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'dev_eui'
, devEui)
.order(
'created_at'
, {
ascending
:
false
});

if
(error) {

throw
error;
}
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.insert(schedule)
.select()
.single();

if
(error) {

throw
error;
}
Found 19 lines of similar code in 3 locations (mass = 83)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.update(updates)
.eq(
'id'
, id)
.select()
.single();

if
(error) {

throw
error;
Found 18 lines of similar code in 6 locations (mass = 70)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.upsert(schedule)
.select()
.single();

if
(error) {

throw
error;
}
Found 15 lines of similar code in 5 locations (mass = 67)
src/lib/repositories/ReportUserScheduleRepository.ts

try
{

const
{ error } =
await

this
.supabase.from(
this
.tableName).delete().eq(
'report_id'
, reportId);

if
(error) {

throw
error;
}
}
catch
(error) {

this
.errorHandler.handleDatabaseError(
error
as

any
,

Error deleting schedules for report: ${reportId}
Found 16 lines of similar code in 6 locations (mass = 74)
src/lib/repositories/RuleRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
', cw_rule_triggered()'
)
.eq(
'profile_id'
, profileId)
.order(
'name'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding rules by profile ID: ${profileId}
Found 21 lines of similar code in 2 locations (mass = 82)
src/lib/repositories/RuleRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.select(
'*'
)
.eq(
'ruleGroupId'
, ruleGroupId)
.single();

if
(error) {

// For "no rows found" error, return null

if
(error.code ===
'PGRST116'
) {

return

null
;
Found 16 lines of similar code in 6 locations (mass = 74)
src/lib/repositories/RuleRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.criteriaTable)
.select(
'*'
)
.eq(
'ruleGroupId'
, ruleGroupId)
.order(
'id'
);

if
(error) {

this
.errorHandler.handleDatabaseError(
error,

Error finding criteria by rule group ID: ${ruleGroupId}
Found 19 lines of similar code in 2 locations (mass = 73)
src/lib/repositories/RuleRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.tableName)
.update(entity)
.eq(
this
.primaryKey, id)
.select(
'*'
)
.single();

if
(error) {

// For "no rows found" error, return null

if
(error.code ===
'PGRST116'
) {
Found 19 lines of similar code in 2 locations (mass = 73)
src/lib/repositories/RuleRepository.ts

const
{ data, error } =
await

this
.supabase
.from(
this
.criteriaTable)
.update(rule)
.eq(
this
.primaryKey, id)
.select(
'*'
)
.single();

if
(error) {

// For "no rows found" error, return null

if
(error.code ===
'PGRST116'
) {
Found 20 lines of similar code in 2 locations (mass = 160)
src/lib/tests/DeviceDataService.timezone.test.ts

// Arrange - This is the exact conversion that was verified to work

const
tokyoMidnight =
new

Date
(
2025
,
7
,
1
,
0
,
0
,
0
,
0
);
// Month is 0-indexed (7 = August)

const
timezone =
'Asia/Tokyo'
;

// Act

const
result = (deviceDataService
as

any
).convertUserTimezoneToUTC(tokyoMidnight, timezone);

// Assert - Must convert to July 31st 15:00 UTC (3PM)

		expect(result.getUTCFullYear()).toBe(

2025
);
expect(result.getUTCMonth()).toBe(
6
);
// 0-indexed (6 = July)
Found 15 lines of similar code in 3 locations (mass = 115)
src/lib/tests/DeviceDataService.timezone.test.ts

// Arrange

const
nyMidnight =
new

Date
(
2025
,
7
,
1
,
0
,
0
,
0
,
0
);

const
timezone =
'America/New_York'
;

// Act

const
result = (deviceDataService
as

any
).convertUserTimezoneToUTC(nyMidnight, timezone);

// Assert - August 1st midnight EST/EDT should convert to 4AM or 5AM UTC (depending on DST)

		expect(result.getUTCFullYear()).toBe(

2025
);
expect(result.getUTCMonth()).toBe(
7
);
// Same month
Found 16 lines of similar code in 2 locations (mass = 107)
src/lib/tests/DeviceDataService.timezone.test.ts

// Arrange - This is the reverse of our critical conversion

const
utcTimestamp =
'2025-07-31T15:00:00.000Z'
;
// July 31st 3PM UTC

const
timezone =
'Asia/Tokyo'
;

// Act

const
result = (deviceDataService
as

any
).convertUTCToUserTimezone(utcTimestamp, timezone);

// Assert - Should convert back to August 1st midnight Tokyo

const
resultDt = DateTime.fromISO(result);
expect(resultDt.year).toBe(
2025
);
Found 20 lines of similar code in 2 locations (mass = 160)
src/lib/tests/NonTrafficDeviceTimezone.test.ts

// Arrange - Same critical test as traffic, but for non-traffic devices

const
tokyoMidnight =
new

Date
(
2025
,
7
,
1
,
0
,
0
,
0
,
0
);
// August 1st midnight

const
timezone =
'Asia/Tokyo'
;

// Act - Use the same timezone conversion method

const
result = (deviceDataService
as

any
).convertUserTimezoneToUTC(tokyoMidnight, timezone);

// Assert - Must convert to July 31st 15:00 UTC (same as traffic)

		expect(result.getUTCFullYear()).toBe(

2025
);
expect(result.getUTCMonth()).toBe(
6
);
// 0-indexed (6 = July)
Found 16 lines of similar code in 2 locations (mass = 107)
src/lib/tests/NonTrafficDeviceTimezone.test.ts

// Arrange - Reverse conversion test

const
utcTimestamp =
'2025-07-31T15:00:00.000Z'
;
// July 31st 3PM UTC

const
timezone =
'Asia/Tokyo'
;

// Act

const
result = (deviceDataService
as

any
).convertUTCToUserTimezone(utcTimestamp, timezone);

// Assert - Should convert back to August 1st midnight Tokyo

const
resultDt = DateTime.fromISO(result);
expect(resultDt.year).toBe(
2025
);
Found 15 lines of similar code in 3 locations (mass = 115)
src/lib/tests/NonTrafficDeviceTimezone.test.ts

// Arrange

const
newYorkMidnight =
new

Date
(
2025
,
7
,
1
,
0
,
0
,
0
,
0
);

const
timezone =
'America/New_York'
;

// Act

const
result = (deviceDataService
as

any
).convertUserTimezoneToUTC(newYorkMidnight, timezone);

// Assert - August 1st midnight EST/EDT should convert to 4AM or 5AM UTC

		expect(result.getUTCFullYear()).toBe(

2025
);
expect(result.getUTCMonth()).toBe(
7
);
// Same month
Found 16 lines of similar code in 3 locations (mass = 115)
src/lib/tests/NonTrafficDeviceTimezone.test.ts

// Arrange

const
londonNoon =
new

Date
(
2025
,
7
,
1
,
12
,
0
,
0
,
0
);

const
timezone =
'Europe/London'
;

// Act

const
result = (deviceDataService
as

any
).convertUserTimezoneToUTC(londonNoon, timezone);

// Assert

		expect(result.getUTCFullYear()).toBe(

2025
);
expect(result.getUTCMonth()).toBe(
7
);
// August
Found 40 lines of similar code in 2 locations (mass = 192)
src/lib/tests/NonTrafficIntegration.test.ts

	it(

'should find water sensors and validate created_at field'
,
async
() => {

// Arrange

const
devices =
await
getDevicesByTable(
'cw_water_data'
);

if
(devices.length ===
0
) {

console
.warn(
'⚠️ No water level devices found, skipping test'
);

return
;
}

const
testDevice = devices[
0
];
Found 40 lines of similar code in 2 locations (mass = 192)
src/lib/tests/NonTrafficIntegration.test.ts

	it(

'should find pulse meters and verify created_at field'
,
async
() => {

// Arrange

const
devices =
await
getDevicesByTable(
'cw_pulse_meters'
);

if
(devices.length ===
0
) {

console
.warn(
'⚠️ No pulse meter devices found, skipping test'
);

return
;
}

const
testDevice = devices[
0
];
Found 20 lines of similar code in 2 locations (mass = 124)
src/lib/tests/PDFReportIntegration.test.ts

// Arrange - Test different timezone parameters

const
testTimezones = [
'Asia/Tokyo'
,
'America/New_York'
,
'Europe/London'
,
'UTC'
];

const
testDate =
'2025-08-01'
;
testTimezones.forEach(
(
tz
) =>
{

// Act - Simulate PDF server timezone conversion

const
date =
new

Date
(testDate);

const
userDate = DateTime.fromJSDate(date).setZone(tz).startOf(
'day'
);

const
utcDate = userDate.toUTC().toJSDate();
Found 19 lines of similar code in 2 locations (mass = 124)
src/lib/tests/PDFReportTimezone.test.ts

// Arrange - Test different timezone scenarios

const
testTimezones = [
'Asia/Tokyo'
,
'America/New_York'
,
'Europe/London'
,
'UTC'
];

const
testDate =
'2025-08-01'
;
testTimezones.forEach(
(
tz
) =>
{

// Act

const
startDate =
new

Date
(testDate);

const
userStartDate = DateTime.fromJSDate(startDate).setZone(tz).startOf(
'day'
);

const
utcStartDate = userStartDate.toUTC().toJSDate();
Found 32 lines of similar code in 2 locations (mass = 172)
src/routes/api/devices/[devEui]/rules/[ruleId]/+server.ts

try
{

const
ruleId =
parseInt
(params.ruleId);

const
data =
await
request.json();

if
(
isNaN
(ruleId)) {

return
json({
error
:
'Invalid rule ID'
}, {
status
:
400
});
}

if
(!data.rule) {

return
json({
error
:
'Rule data is required'
}, {
status
:
400
});
Found 25 lines of similar code in 2 locations (mass = 129)
src/routes/api/devices/[devEui]/rules/[ruleId]/+server.ts

try
{

const
ruleId =
parseInt
(params.ruleId);

if
(
isNaN
(ruleId)) {

return
json({
error
:
'Invalid rule ID'
}, {
status
:
400
});
}

// Create services directly

const
errorHandler =
new
ErrorHandlingService();

const
ruleRepo =
new
RuleRepository(locals.supabase, errorHandler);
Found 32 lines of similar code in 2 locations (mass = 172)
src/routes/api/devices/[devEui]/rules/criteria/[criteriaId]/+server.ts

try
{

const
criteriaId =
parseInt
(params.criteriaId);

const
data =
await
request.json();

if
(
isNaN
(criteriaId)) {

return
json({
error
:
'Invalid criteria ID'
}, {
status
:
400
});
}

if
(!data.criteria) {

return
json({
error
:
'Criteria data is required'
}, {
status
:
400
});
Found 25 lines of similar code in 2 locations (mass = 129)
src/routes/api/devices/[devEui]/rules/criteria/[criteriaId]/+server.ts

try
{

const
criteriaId =
parseInt
(params.criteriaId);

if
(
isNaN
(criteriaId)) {

return
json({
error
:
'Invalid criteria ID'
}, {
status
:
400
});
}

// Create services directly

const
errorHandler =
new
ErrorHandlingService();

const
ruleRepo =
new
RuleRepository(locals.supabase, errorHandler);
Found 32 lines of similar code in 2 locations (mass = 88)
src/routes/app/dashboard/location/[location_id]/+page.server.ts

import
{ error }
from

'@sveltejs/kit'
;
import

type
{ PageServerLoad }
from

'./$types'
;
import
{ ErrorHandlingService }
from

'$lib/errors/ErrorHandlingService'
;
import
{ DeviceRepository }
from

'$lib/repositories/DeviceRepository'
;
import
{ DeviceService }
from

'$lib/services/DeviceService'
;
/**

import
{ error }
from

'@sveltejs/kit'
;
import

type
{ PageServerLoad }
from

'./$types'
;
import
{ ErrorHandlingService }
from

'$lib/errors/ErrorHandlingService'
;
import
{ DeviceRepository }
from

'$lib/repositories/DeviceRepository'
;
import
{ DeviceService }
from

'$lib/services/DeviceService'
;
/**

  • Load devices for a specific location.
  • User session and locationId are already validated in the layout server load.
    */
    Run this check locally
    qlty check --filter=qlty:similar-code --all

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions