Compare commits

..

1 Commits

Author SHA1 Message Date
Sylvain Zimmer
ddac6197e3 (buildpack) add PaaS deployment support, tested with Scalingo 2025-06-01 23:54:35 +02:00
12 changed files with 175 additions and 48 deletions

2
Procfile Normal file
View File

@@ -0,0 +1,2 @@
web: bin/buildpack_start.sh
postdeploy: python manage.py migrate

15
bin/buildpack_postcompile.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-compile script"
rm -rf docker docs env.d gitlint src/frontend/apps/e2e
rm -rf src/frontend/apps
rm -rf src/frontend/packages
# Remove some of the larger packages required by the frontend only
rm -rf src/frontend/node_modules/@next src/frontend/node_modules/next src/frontend/node_modules/react-icons src/frontend/node_modules/@gouvfr-lasuite
# du -ch | sort -rh | head -n 100

15
bin/buildpack_postfrontend.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -o errexit # always exit on error
set -o pipefail # don't ignore exit codes when piping output
echo "-----> Running post-frontend script"
# Move the frontend build to the nginx root and clean up
mkdir -p build/
mv src/frontend/apps/impress/out build/frontend-out
mv src/backend/* ./
mv src/nginx/* ./
echo "3.13" > .python-version

18
bin/buildpack_start.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Start the Django backend server
gunicorn -b :8000 impress.wsgi:application --log-file - &
# Start the Y provider service
cd src/frontend/servers/y-provider && PORT=4444 ../../.scalingo/node/bin/node dist/start-server.js &
# Start the Nginx server
bin/run &
# if the current shell is killed, also terminate all its children
trap "pkill SIGTERM -P $$" SIGTERM
# wait for a single child to finish,
wait -n
# then kill all the other tasks
pkill -P $$

View File

@@ -16,6 +16,7 @@ from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import sentry_sdk
from configurations import Configuration, values
from sentry_sdk.integrations.django import DjangoIntegration
@@ -74,7 +75,9 @@ class Base(Configuration):
# Database
DATABASES = {
"default": {
"default": dj_database_url.config()
if os.environ.get("DATABASE_URL")
else {
"ENGINE": values.Value(
"django.db.backends.postgresql_psycopg2",
environ_name="DB_ENGINE",

View File

@@ -29,6 +29,7 @@ dependencies = [
"boto3==1.38.18",
"Brotli==1.1.0",
"celery[redis]==5.5.2",
"dj-database-url==2.3.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",

View File

@@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next';
import { isAPIError } from '@/api';
import { Box, Icon } from '@/components';
import { useDocOptions, useDocStore } from '@/docs/doc-management/';
import { useAnalytics } from '@/libs';
import {
AITransformActions,
@@ -218,44 +217,22 @@ const AIMenuItemTransform = ({
children,
icon,
}: PropsWithChildren<AIMenuItemTransform>) => {
const { trackEvent } = useAnalytics();
const { mutateAsync: requestAI, isPending } = useDocAITransform();
const editor = useBlockNoteEditor();
const requestAIAction = async (selectedBlocks: Block[]) => {
const text = await editor.blocksToMarkdownLossy(selectedBlocks);
const requestStartTime = performance.now();
const responseAI = await requestAI({
text,
action,
docId,
});
const requestDuration = performance.now() - requestStartTime;
const eventProperties = {
eventName: 'requestAIAction',
action: action,
docId: docId,
requestLength: text.length,
numberBlocks: selectedBlocks.length,
requestDuration: requestDuration,
};
if (!responseAI?.answer) {
trackEvent({
...eventProperties,
status: 'error',
});
throw new Error('No response from AI');
}
trackEvent({
...eventProperties,
status: 'success',
responseLength: String(responseAI.answer),
});
const markdown = await editor.tryParseMarkdownToBlocks(responseAI.answer);
editor.replaceBlocks(selectedBlocks, markdown);
};

View File

@@ -8,14 +8,12 @@ import { useCreateDoc } from '@/docs/doc-management';
import { DocSearchModal } from '@/docs/doc-search';
import { useAuth } from '@/features/auth';
import { useCmdK } from '@/hook/useCmdK';
import { useAnalytics } from '@/libs';
import { useLeftPanelStore } from '../stores';
export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
const router = useRouter();
const { authenticated } = useAuth();
const { trackEvent } = useAnalytics();
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
const openSearchModal = useCallback(() => {
@@ -25,10 +23,8 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
return;
}
trackEvent({ eventName: 'openSearchModal', position: 'LeftPanelHeader' });
setIsSearchModalOpen(true);
}, [trackEvent]);
}, []);
const closeSearchModal = useCallback(() => {
setIsSearchModalOpen(false);
@@ -50,7 +46,6 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => {
};
const createNewDoc = () => {
trackEvent({ eventName: 'createNewDoc', position: 'LeftPanelHeader' });
createDoc();
};

View File

@@ -9,15 +9,7 @@ type AnalyticEventUser = {
email: string;
};
export type AnalyticEventGeneric = {
eventName: string;
[key: string]: string | number;
};
export type AnalyticEvent =
| AnalyticEventClick
| AnalyticEventUser
| AnalyticEventGeneric;
export type AnalyticEvent = AnalyticEventClick | AnalyticEventUser;
export abstract class AbstractAnalytic {
public constructor() {

View File

@@ -3,7 +3,7 @@ import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { JSX, PropsWithChildren, ReactNode, useEffect } from 'react';
import { AbstractAnalytic, AnalyticEvent, AnalyticEventGeneric } from '@/libs/';
import { AbstractAnalytic, AnalyticEvent } from '@/libs/';
export class PostHogAnalytic extends AbstractAnalytic {
private conf?: PostHogConf = undefined;
@@ -19,14 +19,8 @@ export class PostHogAnalytic extends AbstractAnalytic {
}
public trackEvent(evt: AnalyticEvent): void {
switch (evt.eventName) {
case 'user':
posthog.identify(evt.id as string, { email: evt.email });
break;
default:
const { eventName, ...properties } = evt as AnalyticEventGeneric;
posthog.capture(eventName, properties);
break;
if (evt.eventName === 'user') {
posthog.identify(evt.id, { email: evt.email });
}
}

View File

@@ -19,6 +19,7 @@
"app:build": "yarn APP_IMPRESS run build",
"app:test": "yarn APP_IMPRESS run test",
"ci:build": "yarn APP_IMPRESS run build:ci",
"build": "yarn APP_IMPRESS run build && yarn COLLABORATION_SERVER run build",
"e2e:test": "yarn APP_E2E run test",
"lint": "yarn APP_IMPRESS run lint && yarn APP_E2E run lint && yarn workspace eslint-config-impress run lint && yarn I18N run lint && yarn COLLABORATION_SERVER run lint",
"i18n:extract": "yarn I18N run extract-translation",

114
src/nginx/servers.conf.erb Normal file
View File

@@ -0,0 +1,114 @@
# ERB templated nginx configuration
# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx
upstream backend_server {
server localhost:8000 fail_timeout=0;
}
upstream collaboration_server {
server localhost:4444 fail_timeout=0;
}
server {
listen <%= ENV["PORT"] %>;
server_name _;
root /app/build/frontend-out;
error_page 404 /404.html;
location /collaboration/api/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://collaboration_server;
}
location /collaboration/ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# Set appropriate timeout for WebSocketAdd commentMore actions
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://collaboration_server;
}
# Django rest framework
location ^~ /api/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Django admin
location ^~ /admin/ {
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
proxy_pass http://backend_server;
}
# Proxy auth for media
location /media/ {
# Auth request configuration
auth_request /media-auth;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $authDate $upstream_http_x_amz_date;
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
# Pass specific headers from the auth response
proxy_set_header Authorization $authHeader;
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
# Get resource from Object Storage
proxy_pass <%= ENV["AWS_S3_ENDPOINT_URL"] %>/<%= ENV["AWS_STORAGE_BUCKET_NAME"] %>/;
proxy_set_header Host <%= ENV["AWS_S3_ENDPOINT_URL"].split("://")[1] %>;
add_header Content-Security-Policy "default-src 'none'" always;
}
location /media-auth {
proxy_pass http://backend_server/api/v1.0/documents/media-auth/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Original-URL $request_uri;
# Prevent the body from being passed
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
}
location / {
try_files $uri index.html $uri/ =404;
}
location ~ "^/docs/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" {
try_files $uri /docs/[id]/index.html;
}
location = /404.html {
internal;
}
}