Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ paths:
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-status'
/sites/{siteId}/opportunities/{opportunityId}/suggestions/auto-fix:
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-auto-fix'
/sites/{siteId}/opportunities/{opportunityId}/suggestions/edge-live-preview:
$ref: './site-opportunities.yaml#/site-opportunity-suggestions-edge-live-preview'
/sites/{siteId}/site-enrollments:
$ref: './site-enrollments-api.yaml#/site-enrollments-by-site'
/sites/{siteId}/user-activities:
Expand Down
77 changes: 77 additions & 0 deletions docs/openapi/site-opportunities.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,83 @@ site-opportunity-suggestion:
security:
- ims_key: [ ]

site-opportunity-suggestions-edge-live-preview:
parameters:
- $ref: './parameters.yaml#/siteId'
- $ref: './parameters.yaml#/opportunityId'
post:
operationId: edgeLivePreview
summary: |
Fetch live content from a URL using Tokowaka-AI User-Agent
description: |
Fetches live content from a given URL using the Tokowaka-AI User-Agent.
This is useful for previewing what content is currently deployed or accessible
from a specific URL.
tags:
- opportunity-suggestions
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
format: uri
description: The URL to fetch content from
example: https://www.lovesac.com/sactionals
responses:
'200':
description: Successfully fetched content from the URL
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [success, error]
description: Fetch status
statusCode:
type: integer
description: HTTP status code from the URL
message:
type: string
description: Error message if fetch failed
html:
type: object
description: HTML content object (similar to edge-preview response)
properties:
url:
type: string
description: The requested URL
content:
type: string
description: The fetched HTML content (null if fetch failed)
example:
status: success
statusCode: 200
html:
url: https://www.lovesac.com/sactionals
content: "<html>...</html>"
'400':
$ref: './responses.yaml#/400'
'401':
$ref: './responses.yaml#/401'
'404':
$ref: './responses.yaml#/404'
'429':
$ref: './responses.yaml#/429'
'500':
$ref: './responses.yaml#/500'
'503':
$ref: './responses.yaml#/503'
security:
- ims_key: [ ]

site-opportunity-suggestion-fixes:
parameters:
- $ref: './parameters.yaml#/siteId'
Expand Down
110 changes: 110 additions & 0 deletions src/controllers/suggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1298,12 +1298,122 @@ function SuggestionsController(ctx, sqs, env) {
return createResponse(response, 207);
};

/**
* Fetches content from a URL using Tokowaka-AI User-Agent.
* This is a simple URL-based fetch, useful for checking deployed content.
* @param {Object} context of the request
* @returns {Promise<Response>} Fetch response with content
*/
const fetchFromEdge = async (context) => {
const siteId = context.params?.siteId;
const opportunityId = context.params?.opportunityId;

if (!isValidUUID(siteId)) {
return badRequest('Site ID required');
}

if (!isValidUUID(opportunityId)) {
return badRequest('Opportunity ID required');
}

// validate request body
if (!isNonEmptyObject(context.data)) {
return badRequest('No data provided');
}

const { url } = context.data;

// Validate URL
if (!hasText(url)) {
return badRequest('URL is required');
}

// Validate URL format
try {
const parsedUrl = new URL(url); // throws if invalid
if (!parsedUrl.protocol.startsWith('http')) {
return badRequest('Invalid URL format: only HTTP/HTTPS URLs are allowed');
}
} catch (error) {
return badRequest('Invalid URL format');
}

const site = await Site.findById(siteId);
if (!site) {
return notFound('Site not found');
}

if (!await accessControlUtil.hasAccess(site)) {
return forbidden('User does not belong to the organization');
}

const opportunity = await Opportunity.findById(opportunityId);
if (!opportunity || opportunity.getSiteId() !== siteId) {
return notFound('Opportunity not found');
}

try {
context.log.info(`Fetching content from URL: ${url}`);

// Make fetch request with Tokowaka-AI User-Agent
const response = await fetch(url, {
method: 'GET',
headers: {
'User-Agent': 'Tokowaka-AI Tokowaka/1.0',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
},
});

if (!response.ok) {
const requestId = response.headers.get('x-tokowaka-request-id');
const logMessage = requestId
? `Failed to fetch URL. Status: ${response.status}, x-tokowaka-request-id: ${requestId}`
: `Failed to fetch URL. Status: ${response.status}`;
context.log.warn(logMessage);
return ok({
status: 'error',
statusCode: response.status,
message: `Failed to fetch content from URL: ${url}`,
html: {
url,
content: null,
},
});
}

const content = await response.text();

context.log.info(`Successfully fetched content from URL: ${url}`);

return ok({
status: 'success',
statusCode: response.status,
html: {
url,
content,
},
});
} catch (error) {
context.log.error(`Error fetching from URL ${url}: ${error.message}`, error);
return ok({
status: 'error',
statusCode: 500,
message: `Error fetching content: ${error.message}`,
html: {
url,
content: null,
},
});
}
};

return {
autofixSuggestions,
createSuggestions,
deploySuggestionToEdge,
rollbackSuggestionFromEdge,
previewSuggestions,
fetchFromEdge,
getAllForOpportunity,
getAllForOpportunityPaged,
getByID,
Expand Down
1 change: 1 addition & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export default function getRouteHandlers(
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-deploy': suggestionsController.deploySuggestionToEdge,
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-rollback': suggestionsController.rollbackSuggestionFromEdge,
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-preview': suggestionsController.previewSuggestions,
'POST /sites/:siteId/opportunities/:opportunityId/suggestions/edge-live-preview': suggestionsController.fetchFromEdge,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status': suggestionsController.getByStatus,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit/:cursor': suggestionsController.getByStatusPaged,
'GET /sites/:siteId/opportunities/:opportunityId/suggestions/by-status/:status/paged/:limit': suggestionsController.getByStatusPaged,
Expand Down
Loading