From 1d0539f8cac37bc5040f1c60b43643e9a135e6b1 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 10:20:33 +0200 Subject: [PATCH 1/7] Upgrade dependencies --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 0fab22a..8db0591 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 4.1.0 + 4.1.1 @@ -26,18 +26,18 @@ UTF-8 - 11.0.22 + 11.0.25 2.1.0 2.11 - 3.0.3 + 3.1.0 0.8.15 12.10 - 4.2.16.Final - 42.7.12 + 4.2.17.Final + 42.7.13 jacoco From 063d5a7dbcbf6651cd1c9c0245702253d4efacaf Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:23 +0200 Subject: [PATCH 2/7] chore: ignore local Postgres data and macOS .DS_Store Keep the embedded cluster out of git so each machine can reuse its own .local-postgres/ without noise. Co-authored-by: Cursor --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 31b2457..bfb5db3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ WebContent/*.map WebContent/*.js /bin/ src/main/resources/local.application.yaml +.local-postgres/ +.DS_Store From 6c6266ca02543a4d36faf7350b9bd954de715e99 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:30 +0200 Subject: [PATCH 3/7] feat(local): give the anonymous user a stamp, name and id The no-auth chain was building an empty User, so the UI had no owner stamp to list or save questionnaires against. Co-authored-by: Cursor --- .../auth/NoAuthSecurityConfiguration.java | 7 +++++-- .../properties/AnonymousUserProperties.java | 11 +++++++++++ src/main/resources/application.yaml | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/main/java/fr/insee/pogues/configuration/properties/AnonymousUserProperties.java diff --git a/src/main/java/fr/insee/pogues/configuration/auth/NoAuthSecurityConfiguration.java b/src/main/java/fr/insee/pogues/configuration/auth/NoAuthSecurityConfiguration.java index 0ba3c83..af02b2a 100644 --- a/src/main/java/fr/insee/pogues/configuration/auth/NoAuthSecurityConfiguration.java +++ b/src/main/java/fr/insee/pogues/configuration/auth/NoAuthSecurityConfiguration.java @@ -3,6 +3,7 @@ import fr.insee.pogues.configuration.auth.user.User; import fr.insee.pogues.configuration.auth.user.UserProvider; +import fr.insee.pogues.configuration.properties.AnonymousUserProperties; import fr.insee.pogues.configuration.properties.ApplicationProperties; import lombok.AllArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -10,6 +11,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -20,6 +22,7 @@ @ConditionalOnProperty(name = "feature.oidc.enabled", havingValue = "false") @Configuration @EnableWebSecurity +@EnableMethodSecurity @AllArgsConstructor public class NoAuthSecurityConfiguration { private final PublicSecurityFilterChain publicSecurityFilterChainConfiguration; @@ -60,7 +63,7 @@ protected SecurityFilterChain filterPublicUrlsChain(HttpSecurity http, Applicati } @Bean - public UserProvider getUserProvider() { - return auth-> new User(); + public UserProvider getUserProvider(AnonymousUserProperties anonymousUser) { + return auth -> new User(anonymousUser.stamp(), anonymousUser.name(), anonymousUser.id()); } } \ No newline at end of file diff --git a/src/main/java/fr/insee/pogues/configuration/properties/AnonymousUserProperties.java b/src/main/java/fr/insee/pogues/configuration/properties/AnonymousUserProperties.java new file mode 100644 index 0000000..329bb6d --- /dev/null +++ b/src/main/java/fr/insee/pogues/configuration/properties/AnonymousUserProperties.java @@ -0,0 +1,11 @@ +package fr.insee.pogues.configuration.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "feature.anonymous-user") +public record AnonymousUserProperties( + String stamp, + String name, + String id +) { +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9336559..57b4c3a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -27,6 +27,8 @@ application: stamp: restricted: '' corsOrigins: '*' + local: + questionnaires-directory: local-questionnaires pogues-model: version: @pogues-model.version@ @@ -150,6 +152,10 @@ feature: metadata: magma-client: rest # rest | mock ddias-client: rest # rest | mock + anonymous-user: + stamp: default + name: Guest + id: guest oidc: enabled: false auth-server-url: https://auth-server.host/auth From 3cafa01356eacdf84d8517b5f11437300ae9aedc Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:34 +0200 Subject: [PATCH 4/7] feat(local): run Postgres in-process with Zonky instead of Docker Local mode needs a real JSONB/Liquibase database; amd64 binaries ship with the library, ARM Mac and Linux pull extra artifacts, and they stay out of the fat jar. Co-authored-by: Cursor --- pom.xml | 53 +++++++++++++++++++ .../LocalEmbeddedPostgresConfiguration.java | 51 ++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/main/java/fr/insee/pogues/configuration/local/LocalEmbeddedPostgresConfiguration.java diff --git a/pom.xml b/pom.xml index 8db0591..1aec350 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ 25 + local -Xms256m -Xmx512m -ea -Dfile.encoding=UTF-8 UTF-8 @@ -38,6 +39,8 @@ 12.10 4.2.17.Final 42.7.13 + 2.2.2 + 16.15.0 jacoco @@ -46,6 +49,18 @@ java + + + + io.zonky.test.postgres + embedded-postgres-binaries-bom + ${embedded-postgres-binaries.version} + pom + import + + + + @@ -113,6 +128,29 @@ org.postgresql postgresql + + io.zonky.test + embedded-postgres + ${embedded-postgres.version} + true + + + + io.zonky.test.postgres + embedded-postgres-binaries-darwin-arm64v8 + true + + + io.zonky.test.postgres + embedded-postgres-binaries-linux-arm64v8 + true + + + org.springframework.boot + spring-boot-devtools + true + org.liquibase liquibase-core @@ -239,6 +277,12 @@ org.springframework.boot spring-boot-maven-plugin + + repackage + + io.zonky.test,io.zonky.test.postgres + + build-info @@ -271,4 +315,13 @@ + + + local + + local + + + + diff --git a/src/main/java/fr/insee/pogues/configuration/local/LocalEmbeddedPostgresConfiguration.java b/src/main/java/fr/insee/pogues/configuration/local/LocalEmbeddedPostgresConfiguration.java new file mode 100644 index 0000000..84af5bd --- /dev/null +++ b/src/main/java/fr/insee/pogues/configuration/local/LocalEmbeddedPostgresConfiguration.java @@ -0,0 +1,51 @@ +package fr.insee.pogues.configuration.local; + +import com.zaxxer.hikari.HikariDataSource; +import io.zonky.test.db.postgres.embedded.EmbeddedPostgres; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; + +import javax.sql.DataSource; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Starts a real Postgres process (no Docker) for the {@code local} profile. + * Data lives in {@code .local-postgres/} so the cluster is reused across restarts. + */ +@Configuration +@Profile("local") +@Slf4j +public class LocalEmbeddedPostgresConfiguration { + + static final String DATA_DIRECTORY = ".local-postgres"; + static final int PORT = 5433; + + @Bean(destroyMethod = "close") + public EmbeddedPostgres embeddedPostgres() throws IOException { + Path dataDir = Path.of(DATA_DIRECTORY).toAbsolutePath(); + Files.createDirectories(dataDir); + log.info("Starting embedded Postgres in {} on port {}", dataDir, PORT); + return EmbeddedPostgres.builder() + .setPort(PORT) + .setDataDirectory(dataDir) + .setCleanDataDirectory(false) + .setRegisterShutdownHook(false) + .start(); + } + + @Bean(destroyMethod = "close") + @Primary + public DataSource dataSource(EmbeddedPostgres embeddedPostgres) { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:postgresql://localhost:" + embeddedPostgres.getPort() + "/postgres"); + dataSource.setUsername("postgres"); + dataSource.setPassword(""); + dataSource.setMaximumPoolSize(5); + return dataSource; + } +} From afddac275bae50015cd65fa86bdbe0fb6560e021 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:41 +0200 Subject: [PATCH 5/7] feat(local): load questionnaires from a drop folder The folder is the source of truth: on start and on file change, JSON files overwrite the same id, so replacing a sample is no longer a silent no-op. Co-authored-by: Cursor --- local-questionnaires/README.md | 7 + local-questionnaires/qdemo1.json | 128 ++++++++++++++ .../local/LocalQuestionnaireLoader.java | 163 ++++++++++++++++++ .../properties/LocalProperties.java | 9 + src/main/resources/application-local.yaml | 33 ++++ 5 files changed, 340 insertions(+) create mode 100644 local-questionnaires/README.md create mode 100644 local-questionnaires/qdemo1.json create mode 100644 src/main/java/fr/insee/pogues/configuration/local/LocalQuestionnaireLoader.java create mode 100644 src/main/java/fr/insee/pogues/configuration/properties/LocalProperties.java create mode 100644 src/main/resources/application-local.yaml diff --git a/local-questionnaires/README.md b/local-questionnaires/README.md new file mode 100644 index 0000000..86c92f2 --- /dev/null +++ b/local-questionnaires/README.md @@ -0,0 +1,7 @@ +# Local questionnaires + +Drop a Pogues `.json` file here (one questionnaire per file). + +If the API is already running (`mvn spring-boot:run` or the IntelliJ `Pogues (local)` run config), the file is loaded or replaced immediately. Otherwise it is applied on the next start. Then refresh the UI. + +The JSON `id` must be alphanumeric (`qdemo1`, not `q-demo-1`). Files overwrite the same id in the database. Check the API log for `Loaded questionnaire` or `Replaced questionnaire`. diff --git a/local-questionnaires/qdemo1.json b/local-questionnaires/qdemo1.json new file mode 100644 index 0000000..388d779 --- /dev/null +++ b/local-questionnaires/qdemo1.json @@ -0,0 +1,128 @@ +{ + "owner": "FAKEPERMISSION", + "final": false, + "id": "qdemo1", + "Label": [ + "Questionnaire démo (local)" + ], + "Name": "QDEMO1", + "lastUpdatedDate": "2026-09-06T08:00:00.000Z", + "DataCollection": [], + "genericName": "QUESTIONNAIRE", + "ComponentGroup": [], + "agency": "fr.insee", + "TargetMode": [ + "CAPI", + "CAWI" + ], + "flowLogic": "FILTER", + "formulasLanguage": "VTL", + "childQuestionnaireRef": [], + "Child": [ + { + "id": "qdemo1seq1", + "depth": 1, + "Name": "SEQDEMO", + "Label": [ + "Séquence démo" + ], + "Declaration": [], + "Control": [], + "FlowControl": [], + "TargetMode": [ + "CAPI", + "CAWI" + ], + "type": "SequenceType", + "genericName": "MODULE", + "Child": [ + { + "id": "qdemo1q1", + "depth": 2, + "Name": "DEMOVAR", + "Label": [ + "Quelle est votre réponse démo ?" + ], + "Declaration": [], + "Control": [], + "FlowControl": [], + "TargetMode": [ + "CAPI", + "CAWI" + ], + "type": "QuestionType", + "questionType": "SIMPLE", + "ClarificationQuestion": [], + "Response": [ + { + "id": "qdemo1resp1", + "Datatype": { + "typeName": "TEXT", + "type": "TextDatatypeType", + "MaxLength": 50, + "Pattern": "" + }, + "CollectedVariableReference": "qdemo1var1", + "mandatory": false + } + ] + } + ] + }, + { + "id": "idendquest", + "depth": 1, + "Name": "QUESTIONNAIRE_END", + "Label": [ + "QUESTIONNAIRE_END" + ], + "Declaration": [], + "genericName": "MODULE", + "Control": [], + "FlowControl": [], + "type": "SequenceType", + "Child": [], + "TargetMode": [ + "CAPI", + "CAWI" + ] + } + ], + "CodeLists": { + "CodeList": [ + { + "id": "qdemo1cl1", + "Label": "Liste démo", + "Code": [ + { + "Label": "Oui", + "Value": "1", + "Parent": "" + }, + { + "Label": "Non", + "Value": "2", + "Parent": "" + } + ] + } + ] + }, + "Variables": { + "Variable": [ + { + "id": "qdemo1var1", + "Name": "DEMOVAR", + "Label": "Variable démo", + "type": "CollectedVariableType", + "Datatype": { + "typeName": "TEXT", + "type": "TextDatatypeType", + "MaxLength": 50, + "Pattern": "" + } + } + ] + }, + "FlowControl": [] +} diff --git a/src/main/java/fr/insee/pogues/configuration/local/LocalQuestionnaireLoader.java b/src/main/java/fr/insee/pogues/configuration/local/LocalQuestionnaireLoader.java new file mode 100644 index 0000000..3630126 --- /dev/null +++ b/src/main/java/fr/insee/pogues/configuration/local/LocalQuestionnaireLoader.java @@ -0,0 +1,163 @@ +package fr.insee.pogues.configuration.local; + +import fr.insee.pogues.configuration.properties.AnonymousUserProperties; +import fr.insee.pogues.configuration.properties.LocalProperties; +import fr.insee.pogues.persistence.repository.QuestionnaireRepository; +import fr.insee.pogues.persistence.service.IQuestionnaireService; +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.time.Instant; +import java.util.Locale; +import java.util.stream.Stream; + +import static fr.insee.pogues.utils.json.JSONFunctions.jsonStringtoJsonNode; + +/** + * Loads Pogues JSON files dropped in {@code local-questionnaires/}. + * The folder is the source of truth: on startup and on file change, files overwrite + * questionnaires with the same id. Logged at INFO so a replace is never silent. + */ +@Component +@Profile("local") +@RequiredArgsConstructor +@Slf4j +public class LocalQuestionnaireLoader implements CommandLineRunner { + + private static final String QUESTIONNAIRE_ID_PATTERN = "[a-zA-Z0-9]+"; + private static final long FILE_SETTLE_MS = 400; + + private final LocalProperties localProperties; + private final AnonymousUserProperties anonymousUser; + private final QuestionnaireRepository questionnaireRepository; + private final IQuestionnaireService questionnaireService; + + private volatile WatchService watchService; + + @Override + public void run(String... args) throws IOException { + Path directory = resolveDirectory(); + Files.createDirectories(directory); + loadAll(directory); + startWatching(directory); + log.info( + "Local questionnaires: files in {} overwrite the same id in the database (startup and drop)", + directory.toAbsolutePath()); + } + + private Path resolveDirectory() { + String configured = localProperties.questionnairesDirectory(); + if (configured == null || configured.isBlank()) { + configured = "local-questionnaires"; + } + return Path.of(configured).toAbsolutePath().normalize(); + } + + private void loadAll(Path directory) { + try (Stream files = Files.list(directory)) { + files.filter(this::isJsonFile).forEach(this::loadFile); + } catch (IOException e) { + log.error("Failed to scan {}", directory, e); + } + } + + private void startWatching(Path directory) throws IOException { + watchService = FileSystems.getDefault().newWatchService(); + directory.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY); + Thread watcher = new Thread(() -> watchLoop(directory), "local-questionnaires-watch"); + watcher.setDaemon(true); + watcher.start(); + } + + private void watchLoop(Path directory) { + try { + while (watchService != null) { + WatchKey key = watchService.take(); + for (WatchEvent event : key.pollEvents()) { + if (event.kind() == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path filename = (Path) event.context(); + Path file = directory.resolve(filename); + Thread.sleep(FILE_SETTLE_MS); + if (!isJsonFile(file)) { + continue; + } + loadFile(file); + } + if (!key.reset()) { + break; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private boolean isJsonFile(Path file) { + String name = file.getFileName().toString().toLowerCase(Locale.ROOT); + return Files.isRegularFile(file) && name.endsWith(".json"); + } + + private void loadFile(Path file) { + String filename = file.getFileName().toString(); + try { + String raw = Files.readString(file, StandardCharsets.UTF_8); + JsonNode node = jsonStringtoJsonNode(raw); + if (!(node instanceof ObjectNode questionnaire)) { + log.warn("Skip {}: root must be a JSON object", filename); + return; + } + JsonNode idNode = questionnaire.get("id"); + if (idNode == null || idNode.asString() == null || idNode.asString().isBlank()) { + log.warn("Skip {}: missing id", filename); + return; + } + String id = idNode.asString(); + if (!id.matches(QUESTIONNAIRE_ID_PATTERN)) { + log.warn("Skip {}: id '{}' must be alphanumeric (no hyphen)", filename, id); + return; + } + questionnaire.put("owner", anonymousUser.stamp()); + if (questionnaire.get("lastUpdatedDate") == null + || questionnaire.get("lastUpdatedDate").asString().isBlank()) { + questionnaire.put("lastUpdatedDate", Instant.now().toString()); + } + boolean existed = questionnaireRepository.getQuestionnaireByID(id) != null; + if (existed) { + questionnaireService.updateQuestionnaire(id, questionnaire); + log.info("Replaced questionnaire {} from {} (overwrote database)", id, filename); + } else { + questionnaireService.createQuestionnaire(questionnaire); + log.info("Loaded questionnaire {} from {}", id, filename); + } + } catch (Exception e) { + log.warn("Skip {}: {}", filename, e.getMessage()); + } + } + + @PreDestroy + void stopWatching() throws IOException { + if (watchService != null) { + watchService.close(); + } + } +} diff --git a/src/main/java/fr/insee/pogues/configuration/properties/LocalProperties.java b/src/main/java/fr/insee/pogues/configuration/properties/LocalProperties.java new file mode 100644 index 0000000..9f57a44 --- /dev/null +++ b/src/main/java/fr/insee/pogues/configuration/properties/LocalProperties.java @@ -0,0 +1,9 @@ +package fr.insee.pogues.configuration.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "application.local") +public record LocalProperties( + String questionnairesDirectory +) { +} diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml new file mode 100644 index 0000000..e7c7872 --- /dev/null +++ b/src/main/resources/application-local.yaml @@ -0,0 +1,33 @@ +# Local full-stack: no Keycloak, mocked metadata, embedded Postgres (no Docker). +# Start with: mvn spring-boot:run (or the Pogues main class from the IDE) +# Port 8081 avoids macOS AirPlay on 5000 and Keycloak on 8080. + +server: + port: 8081 + +spring: + liquibase: + enabled: true + contexts: prod + +feature: + oidc: + enabled: false + metadata: + magma-client: mock + ddias-client: mock + anonymous-user: + stamp: FAKEPERMISSION + name: Guybrush + id: guybrush + cache: + enabled: false + +application: + corsOrigins: + - http://localhost:5173 + - http://127.0.0.1:5173 + - http://localhost:4173 + - http://localhost:5145 + local: + questionnaires-directory: local-questionnaires From e371cddaed17f78c0dd99ced04a359d419c39814 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:46 +0200 Subject: [PATCH 6/7] feat(local): default the local profile from the IDE and Maven A workspace classpath with no explicit profile now starts as local, and IntelliJ gets a shared Pogues (local) run configuration. Packaged jars stay on production defaults. Co-authored-by: Cursor --- .run/Pogues local.run.xml | 10 +++++++ src/main/java/fr/insee/pogues/Pogues.java | 24 +++++++++++++++++ .../BuildPropertiesFallbackConfiguration.java | 26 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 .run/Pogues local.run.xml create mode 100644 src/main/java/fr/insee/pogues/configuration/BuildPropertiesFallbackConfiguration.java diff --git a/.run/Pogues local.run.xml b/.run/Pogues local.run.xml new file mode 100644 index 0000000..54ed17a --- /dev/null +++ b/.run/Pogues local.run.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/src/main/java/fr/insee/pogues/Pogues.java b/src/main/java/fr/insee/pogues/Pogues.java index aa483bd..9272088 100644 --- a/src/main/java/fr/insee/pogues/Pogues.java +++ b/src/main/java/fr/insee/pogues/Pogues.java @@ -28,9 +28,33 @@ public static SpringApplicationBuilder configureApplicationBuilder(SpringApplica } public static void main(String[] args) { + if (shouldDefaultToLocalProfile()) { + System.setProperty("spring.profiles.default", "local"); + log.info("No Spring profile specified, using 'local'"); + } configureApplicationBuilder(new SpringApplicationBuilder()).build().run(args); } + /** + * IDE / {@code mvn spring-boot:run} put {@code target/classes} on the classpath. + * A packaged jar does not, so production is left untouched. + */ + static boolean shouldDefaultToLocalProfile() { + if (hasText(System.getenv("SPRING_PROFILES_ACTIVE")) + || hasText(System.getProperty("spring.profiles.active")) + || hasText(System.getProperty("spring.profiles.default"))) { + return false; + } + String classpath = System.getProperty("java.class.path", ""); + return classpath.contains("target/classes") + || classpath.contains("out/production") + || classpath.contains("out\\production"); + } + + private static boolean hasText(String value) { + return value != null && !value.isBlank(); + } + @PostConstruct public void executeAfterMain() { log.info("Timezone is set to '{}'", applicationTimeZoneId); diff --git a/src/main/java/fr/insee/pogues/configuration/BuildPropertiesFallbackConfiguration.java b/src/main/java/fr/insee/pogues/configuration/BuildPropertiesFallbackConfiguration.java new file mode 100644 index 0000000..0f629dd --- /dev/null +++ b/src/main/java/fr/insee/pogues/configuration/BuildPropertiesFallbackConfiguration.java @@ -0,0 +1,26 @@ +package fr.insee.pogues.configuration; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +/** + * Maven {@code build-info} is not generated when the app is started from the IDE. + */ +@Configuration +public class BuildPropertiesFallbackConfiguration { + + @Bean + @ConditionalOnMissingBean(BuildProperties.class) + public BuildProperties buildProperties() { + Properties properties = new Properties(); + properties.setProperty("group", "fr.insee"); + properties.setProperty("artifact", "Pogues-BO"); + properties.setProperty("name", "Pogues-Back-Office"); + properties.setProperty("version", "dev"); + return new BuildProperties(properties); + } +} From f85d60afcad1423fc72aee45ae059bb175563bd8 Mon Sep 17 00:00:00 2001 From: Nicolas Laval Date: Sun, 6 Sep 2026 19:56:51 +0200 Subject: [PATCH 7/7] docs: explain how to run the API locally without Keycloak Document the local profile, the drop folder, and that files overwrite the same questionnaire id. Co-authored-by: Cursor --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 90b6aac..96fad91 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pogues +# Pogues API [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=InseeFr_Pogues-Back-Office&metric=alert_status)](https://sonarcloud.io/dashboard?id=InseeFr_Pogues-Back-Office) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=InseeFr_Pogues-Back-Office&metric=security_rating)](https://sonarcloud.io/dashboard?id=InseeFr_Pogues-Back-Office) @@ -15,3 +15,42 @@ Navigation: [Client][1] | **Back-office** | [Pogues model][2] Pogues is a tool that allow to design questionnaires with components that are structural (sequences, questions...) and dynamic (filters, controls, loops...). This is the repository of the back-end part of Pogues. + +## Local run with the Pogues front (no Keycloak) + +The `local` profile uses anonymous auth (Guybrush / `FAKEPERMISSION`), mocks Magma and DDI-AS, and embeds Postgres (no Docker). + +Prerequisites: Java 25, Maven. Same commands on macOS, Linux and Windows (PowerShell, Git Bash or cmd). + +From IntelliJ: run the shared `Pogues (local)` configuration, or start `fr.insee.pogues.Pogues` (the `local` profile is selected on its own in a workspace classpath). + +From the command line: + +```bash +mvn spring-boot:run +``` + +That starts Postgres (port 5433, data in `.local-postgres/`) then the API on http://localhost:8081. Swagger: http://localhost:8081/ + +The first start can take a while: Postgres binaries are extracted once. + +In the Pogues frontend repo, in parallel: + +```bash +pnpm --dir next install +pnpm dev:api +``` + +→ http://localhost:5173 + +### Add a questionnaire + +Drop a Pogues `.json` file in `local-questionnaires/` (this repo root). The API picks it up on its own; refresh the UI. + +One questionnaire per file. The JSON `id` must be alphanumeric (`qdemo1`, no hyphen). The folder wins: on start and on drop, a file overwrites the same id in the database (logged as `Replaced questionnaire …`). UI edits on that id are lost when you restart or replace the file. + +`qdemo1.json` is the sample loaded on the first start. + +`.local-postgres/` is kept across restarts so Postgres binaries are not extracted every time. Delete it only if you want a full schema wipe (API stopped). + +Visualization (Eno, Queen, Stromae) is not started in this mode.