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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Overroot is independent and unaffiliated with Overleaf or Underleaf.ai.
- PDF viewer that preserves page, zoom, and exact scroll position after recompilation
- One-click PDF-only mode with `Escape` to return
- Article, report, presentation, and blank templates; file uploads and ZIP import/export
- Academic Classic, Modern ML, and Visual Results poster templates with A0, A1, and custom print sizes
- Light, dark, and system appearance across desktop and mobile layouts
- Project-scoped, revocable credentials for external agents
- SQLite persistence plus an online backup command with bounded snapshot retention
Expand Down Expand Up @@ -86,6 +87,8 @@ exports always use the original project files.

Open a `.tex` file and select **Compile** to flush the current collaborative editor state and run Tectonic on that file. Every editor can compile the file they opened; no owner-selected main file is needed. Successful output appears in the PDF pane for every connected collaborator. A completely empty document body has no printable page, so add content between `\begin{document}` and `\end{document}` before compiling a blank template.

For a poster, choose one of the poster templates at project creation and set its size, orientation, columns, and heading metadata. Edit the modular `sections/*.tex` files in **Files**, compile `poster.tex`, and review the PDF's measured page count and physical dimensions before downloading. See [poster guide](docs/POSTERS.md).

## External agent access

Overroot does not embed an AI model or chat interface. A project owner can authorize an external coding agent to edit the project directly:
Expand Down Expand Up @@ -134,7 +137,7 @@ Requirements, host setup, runtime paths, deployment commands, and recovery are d

## Compilation isolation

The first real compile builds `overroot-tectonic:0.16.9` if necessary and starts `overroot-tectonic-worker`. It runs as an unprivileged user with dropped capabilities, a read-only root filesystem, PID/CPU/memory limits, Tectonic `--untrusted`, and a compile timeout. Only the jobs directory is writable; downloaded packages remain in the `overroot-tectonic-cache` Docker volume.
The first real compile builds `overroot-tectonic:0.16.9-posters-v1` if necessary and starts `overroot-tectonic-worker`. It runs as an unprivileged user with dropped capabilities, a read-only root filesystem, PID/CPU/memory limits, Tectonic `--untrusted`, and a compile timeout. Only the jobs directory is writable; downloaded packages remain in the `overroot-tectonic-cache` Docker volume.

The cache key includes the entry file, compiler context, flags, paths, and every source revision. An identical project state reuses its existing PDF immediately.

Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const config = {
compileTimeoutSeconds: integer('OVERROOT_COMPILE_TIMEOUT_SECONDS', 45),
compileMaxJobs: integer('OVERROOT_COMPILE_MAX_JOBS', 8),
compileMaxSnapshotBytes: integer('OVERROOT_COMPILE_MAX_SNAPSHOT_BYTES', 128 * 1024 * 1024),
dockerImage: process.env.OVERROOT_DOCKER_IMAGE ?? 'overroot-tectonic:0.16.9',
dockerImage: process.env.OVERROOT_DOCKER_IMAGE ?? 'overroot-tectonic:0.16.9-posters-v1',
dockerContainer: process.env.OVERROOT_DOCKER_CONTAINER ?? 'overroot-tectonic-worker',
fakeCompiler: bool('OVERROOT_FAKE_COMPILER', false),
trustProxy: bool('OVERROOT_TRUST_PROXY', true),
Expand Down
58 changes: 58 additions & 0 deletions apps/server/src/poster-template-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Adapted from Overposter (MIT); see docs/POSTERS.md.
export const posterLicense = `MIT License

Copyright (c) 2026 Overposter contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
`;
export const posterTemplateFiles: Record<string, Record<string, string>> = {
"academic-classic": {
"poster.tex": "\\RequirePackage{fix-cm}\n\\documentclass{article}\n\\input{config.tex}\n\\usepackage[paperwidth=\\PosterWidth mm,paperheight=\\PosterHeight mm,margin=25mm]{geometry}\n\\usepackage{xcolor}\n\\definecolor{accent}{HTML}{23496C}\n\\definecolor{ink}{HTML}{172B40}\n\\input{poster-layout.tex}\n\\newcommand{\\PosterHeading}[1]{{\\color{accent}\\fontsize{\\PosterHeadingSize pt}{42pt}\\selectfont\\bfseries #1}\\par\\hrule height 1.2pt}\n\\begin{document}\n\\IfFileExists{assets/logo.pdf}{\\noindent\\includegraphics[height=25mm]{assets/logo.pdf}\\par\\vspace{4mm}}{}\n\\noindent\\begin{minipage}[c]{\\linewidth}\n{\\color{accent}\\fontsize{\\PosterTitleSize pt}{70pt}\\selectfont\\bfseries\\PosterTitle\\par}\n\\vspace{3mm}{\\fontsize{28}{35}\\selectfont\\PosterSubtitle\\par}\n\\vspace{5mm}\\PosterMetadata\n\\end{minipage}\n\\par\\vspace{5mm}{\\color{accent}\\hrule height 3pt}\\vspace{7mm}\n\\PosterGrid{\n\\PosterBlock{Motivation and question}{\\input{sections/introduction.tex}}\n\\PosterBlock{Method}{\\input{sections/methodology.tex}}\n\\PosterBlock{Study design}{\\input{sections/study.tex}}\n\\PosterBlock{Results}{\\input{sections/results.tex}}\n\\PosterBlock{Interpretation}{\\input{sections/conclusions.tex}}\n\\PosterBlock{References}{\\input{sections/references.tex}}\n\\PosterContactBlock\n}\n\\end{document}\n",
"references.bib": "% Add verified BibTeX entries.\n",
"sections/conclusions.tex": "Summarize the supported finding, limitations, and next step.\n",
"sections/introduction.tex": "State the problem, why it matters, and a precise research question. Replace this guidance with your own evidence and citations.\n",
"sections/methodology.tex": "Explain the method and its assumptions. Equations can be included, for example $\\hat y=f_{\\theta}(x)$.\n",
"sections/references.tex": "Add verified references here. Bibliography data can be stored in \\texttt{references.bib}.\n",
"sections/results.tex": "Add a table, uncertainty intervals, and a visual comparison. Clearly label any illustrative numbers.\n",
"sections/study.tex": "Describe the dataset, baselines, and evaluation protocol. Add a vector diagram in \\texttt{figures/} and include it with \\texttt{\\textbackslash includegraphics}.\n",
"poster-layout.tex": "\\usepackage{amsmath,amssymb,graphicx,xcolor,tikz,multicol,qrcode,hyperref}\n\\hypersetup{colorlinks=true,urlcolor=accent}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0pt}\n\\setlength{\\columnsep}{12mm}\n\\newcommand{\\PosterBody}{\\fontsize{\\PosterBodySize pt}{\\PosterBodyLeading pt}\\selectfont}\n\\newcommand{\\PosterMetadata}{%\n \\ifx\\PosterAuthors\\empty\\else\n {\\fontsize{30}{38}\\selectfont\\PosterAuthors\\par}\n \\fi\n \\ifx\\PosterAffiliation\\empty\\else\n \\vspace{3mm}{\\fontsize{22}{29}\\selectfont\\PosterAffiliation\\par}\n \\fi\n}\n\\newcommand{\\PosterBlock}[2]{%\n \\par\\vspace{5mm}\\noindent\n \\begin{minipage}{\\linewidth}\n \\PosterHeading{#1}\\par\\vspace{3mm}\n {\\PosterBody #2\\par}\\vspace{3mm}\n \\end{minipage}\\par\n}\n\\newcommand{\\PosterGrid}[1]{\\begin{multicols}{\\PosterColumns}#1\\end{multicols}}\n\\newcommand{\\PosterContactBlock}{%\n \\ifx\\PosterPaperURL\\empty\\else\n \\par\\vspace{6mm}\n \\noindent\\begin{minipage}[c]{39mm}\\qrcode[height=35mm]{\\PosterPaperURL}\\end{minipage}%\n \\hspace{5mm}\\begin{minipage}[c]{\\dimexpr\\linewidth-46mm\\relax}\n {\\fontsize{21}{28}\\selectfont Paper and supplementary material\\par\n \\url{\\PosterPaperURL}}\n \\end{minipage}\\par\n \\fi\n \\ifx\\PosterContact\\empty\\else\n \\par\\vspace{4mm}{\\fontsize{21}{28}\\selectfont\\PosterContact\\par}\n \\fi\n}\n"
},
"modern-ml": {
"poster.tex": "\\RequirePackage{fix-cm}\n\\documentclass{article}\n\\input{config.tex}\n\\usepackage[paperwidth=\\PosterWidth mm,paperheight=\\PosterHeight mm,margin=24mm]{geometry}\n\\usepackage{xcolor}\n\\definecolor{accent}{HTML}{0D766E}\n\\definecolor{ink}{HTML}{153448}\n\\definecolor{pale}{HTML}{E8F3F0}\n\\input{poster-layout.tex}\n\\newcommand{\\PosterHeading}[1]{{\\color{accent}\\fontsize{\\PosterHeadingSize pt}{42pt}\\selectfont\\bfseries #1}\\par}\n\\begin{document}\n\\noindent\\colorbox{ink}{\\begin{minipage}{\\dimexpr\\linewidth-2\\fboxsep\\relax}\n\\vspace{8mm}\\hspace{8mm}\\begin{minipage}[c]{0.80\\linewidth}\n{\\color{white}\\fontsize{\\PosterTitleSize pt}{70pt}\\selectfont\\bfseries\\PosterTitle\\par}\n\\vspace{2mm}{\\color{white}\\fontsize{27}{34}\\selectfont\\PosterSubtitle\\par}\n\\vspace{4mm}{\\color{white}\\PosterMetadata}\n\\end{minipage}\n\\IfFileExists{assets/logo.pdf}{\\hfill\\begin{minipage}[c]{0.13\\linewidth}\\includegraphics[width=\\linewidth]{assets/logo.pdf}\\end{minipage}}{}\n\\vspace{8mm}\n\\end{minipage}}\\par\\vspace{8mm}\n\\noindent\\colorbox{pale}{\\begin{minipage}{\\dimexpr\\linewidth-2\\fboxsep\\relax}\\vspace{5mm}\\hspace{7mm}\\begin{minipage}{0.94\\linewidth}\n{\\color{ink}\\fontsize{25}{32}\\selectfont\\bfseries Key takeaway\\par}\n\\vspace{2mm}{\\PosterBody\\input{sections/conclusions.tex}\\par}\n\\end{minipage}\\vspace{5mm}\\end{minipage}}\\par\\vspace{5mm}\n\\PosterGrid{\n\\PosterBlock{The question}{\\input{sections/introduction.tex}}\n\\PosterBlock{Approach}{\\input{sections/methodology.tex}}\n\\PosterBlock{Experimental setup}{\\input{sections/study.tex}}\n\\PosterBlock{Main result}{\\input{sections/results.tex}}\n\\PosterBlock{Sources}{\\input{sections/references.tex}}\n\\PosterContactBlock\n}\n\\end{document}\n",
"references.bib": "% Add verified BibTeX entries.\n",
"sections/conclusions.tex": "Summarize the supported finding, limitations, and next step.\n",
"sections/introduction.tex": "State the problem, why it matters, and a precise research question. Replace this guidance with your own evidence and citations.\n",
"sections/methodology.tex": "Explain the method and its assumptions. Equations can be included, for example $\\hat y=f_{\\theta}(x)$.\n",
"sections/references.tex": "Add verified references here. Bibliography data can be stored in \\texttt{references.bib}.\n",
"sections/results.tex": "Add a table, uncertainty intervals, and a visual comparison. Clearly label any illustrative numbers.\n",
"sections/study.tex": "Describe the dataset, baselines, and evaluation protocol. Add a vector diagram in \\texttt{figures/} and include it with \\texttt{\\textbackslash includegraphics}.\n",
"poster-layout.tex": "\\usepackage{amsmath,amssymb,graphicx,xcolor,tikz,multicol,qrcode,hyperref}\n\\hypersetup{colorlinks=true,urlcolor=accent}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0pt}\n\\setlength{\\columnsep}{12mm}\n\\newcommand{\\PosterBody}{\\fontsize{\\PosterBodySize pt}{\\PosterBodyLeading pt}\\selectfont}\n\\newcommand{\\PosterMetadata}{%\n \\ifx\\PosterAuthors\\empty\\else\n {\\fontsize{30}{38}\\selectfont\\PosterAuthors\\par}\n \\fi\n \\ifx\\PosterAffiliation\\empty\\else\n \\vspace{3mm}{\\fontsize{22}{29}\\selectfont\\PosterAffiliation\\par}\n \\fi\n}\n\\newcommand{\\PosterBlock}[2]{%\n \\par\\vspace{5mm}\\noindent\n \\begin{minipage}{\\linewidth}\n \\PosterHeading{#1}\\par\\vspace{3mm}\n {\\PosterBody #2\\par}\\vspace{3mm}\n \\end{minipage}\\par\n}\n\\newcommand{\\PosterGrid}[1]{\\begin{multicols}{\\PosterColumns}#1\\end{multicols}}\n\\newcommand{\\PosterContactBlock}{%\n \\ifx\\PosterPaperURL\\empty\\else\n \\par\\vspace{6mm}\n \\noindent\\begin{minipage}[c]{39mm}\\qrcode[height=35mm]{\\PosterPaperURL}\\end{minipage}%\n \\hspace{5mm}\\begin{minipage}[c]{\\dimexpr\\linewidth-46mm\\relax}\n {\\fontsize{21}{28}\\selectfont Paper and supplementary material\\par\n \\url{\\PosterPaperURL}}\n \\end{minipage}\\par\n \\fi\n \\ifx\\PosterContact\\empty\\else\n \\par\\vspace{4mm}{\\fontsize{21}{28}\\selectfont\\PosterContact\\par}\n \\fi\n}\n"
},
"visual-results": {
"poster.tex": "\\RequirePackage{fix-cm}\n\\documentclass{article}\n\\input{config.tex}\n\\usepackage[paperwidth=\\PosterWidth mm,paperheight=\\PosterHeight mm,margin=24mm]{geometry}\n\\usepackage{xcolor}\n\\definecolor{accent}{HTML}{A54431}\n\\definecolor{ink}{HTML}{25323A}\n\\definecolor{pale}{HTML}{F7EDE8}\n\\input{poster-layout.tex}\n\\newcommand{\\PosterHeading}[1]{{\\color{accent}\\fontsize{\\PosterHeadingSize pt}{42pt}\\selectfont\\bfseries #1}\\par}\n\\begin{document}\n{\\color{accent}\\hrule height 7pt}\\vspace{6mm}\n\\IfFileExists{assets/logo.pdf}{\\noindent\\includegraphics[height=25mm]{assets/logo.pdf}\\par\\vspace{4mm}}{}\n\\noindent\\begin{minipage}[c]{\\linewidth}\n{\\color{ink}\\fontsize{\\PosterTitleSize pt}{70pt}\\selectfont\\bfseries\\PosterTitle\\par}\n\\vspace{3mm}{\\fontsize{27}{34}\\selectfont\\PosterSubtitle\\par}\n\\vspace{5mm}\\PosterMetadata\n\\end{minipage}\n\\par\\vspace{8mm}\n\\noindent\\colorbox{pale}{\\begin{minipage}{\\dimexpr\\linewidth-2\\fboxsep\\relax}\\vspace{5mm}\\hspace{7mm}\\begin{minipage}{0.94\\linewidth}\n\\PosterHeading{Visual evidence}\\vspace{3mm}{\\PosterBody\\input{sections/study.tex}\\par}\n\\end{minipage}\\vspace{5mm}\\end{minipage}}\\par\\vspace{8mm}\n\\PosterGrid{\n\\PosterBlock{Question}{\\input{sections/introduction.tex}}\n\\PosterBlock{Method}{\\input{sections/methodology.tex}}\n\\PosterBlock{Measured outcomes}{\\input{sections/results.tex}}\n\\PosterBlock{What to remember}{\\input{sections/conclusions.tex}}\n\\PosterBlock{References}{\\input{sections/references.tex}}\n\\PosterContactBlock\n}\n\\end{document}\n",
"references.bib": "% Add verified BibTeX entries.\n",
"sections/conclusions.tex": "Summarize the supported finding, limitations, and next step.\n",
"sections/introduction.tex": "State the problem, why it matters, and a precise research question. Replace this guidance with your own evidence and citations.\n",
"sections/methodology.tex": "Explain the method and its assumptions. Equations can be included, for example $\\hat y=f_{\\theta}(x)$.\n",
"sections/references.tex": "Add verified references here. Bibliography data can be stored in \\texttt{references.bib}.\n",
"sections/results.tex": "Add a table, uncertainty intervals, and a visual comparison. Clearly label any illustrative numbers.\n",
"sections/study.tex": "Describe the dataset, baselines, and evaluation protocol. Add a vector diagram in \\texttt{figures/} and include it with \\texttt{\\textbackslash includegraphics}.\n",
"poster-layout.tex": "\\usepackage{amsmath,amssymb,graphicx,xcolor,tikz,multicol,qrcode,hyperref}\n\\hypersetup{colorlinks=true,urlcolor=accent}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0pt}\n\\setlength{\\columnsep}{12mm}\n\\newcommand{\\PosterBody}{\\fontsize{\\PosterBodySize pt}{\\PosterBodyLeading pt}\\selectfont}\n\\newcommand{\\PosterMetadata}{%\n \\ifx\\PosterAuthors\\empty\\else\n {\\fontsize{30}{38}\\selectfont\\PosterAuthors\\par}\n \\fi\n \\ifx\\PosterAffiliation\\empty\\else\n \\vspace{3mm}{\\fontsize{22}{29}\\selectfont\\PosterAffiliation\\par}\n \\fi\n}\n\\newcommand{\\PosterBlock}[2]{%\n \\par\\vspace{5mm}\\noindent\n \\begin{minipage}{\\linewidth}\n \\PosterHeading{#1}\\par\\vspace{3mm}\n {\\PosterBody #2\\par}\\vspace{3mm}\n \\end{minipage}\\par\n}\n\\newcommand{\\PosterGrid}[1]{\\begin{multicols}{\\PosterColumns}#1\\end{multicols}}\n\\newcommand{\\PosterContactBlock}{%\n \\ifx\\PosterPaperURL\\empty\\else\n \\par\\vspace{6mm}\n \\noindent\\begin{minipage}[c]{39mm}\\qrcode[height=35mm]{\\PosterPaperURL}\\end{minipage}%\n \\hspace{5mm}\\begin{minipage}[c]{\\dimexpr\\linewidth-46mm\\relax}\n {\\fontsize{21}{28}\\selectfont Paper and supplementary material\\par\n \\url{\\PosterPaperURL}}\n \\end{minipage}\\par\n \\fi\n \\ifx\\PosterContact\\empty\\else\n \\par\\vspace{4mm}{\\fontsize{21}{28}\\selectfont\\PosterContact\\par}\n \\fi\n}\n"
}
};
62 changes: 62 additions & 0 deletions apps/server/src/posters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { z } from 'zod';
import { posterLicense, posterTemplateFiles } from './poster-template-files.js';

export const posterThemes = ['academic-classic', 'modern-ml', 'visual-results'] as const;
export const posterOptionsSchema = z.object({
subtitle: z.string().trim().max(240).default(''),
authors: z.string().trim().max(500).default(''),
affiliation: z.string().trim().max(500).default(''),
size: z.enum(['A0', 'A1', 'custom']).default('A1'),
orientation: z.enum(['landscape', 'portrait']).default('landscape'),
widthMm: z.number().int().min(200).max(3000).optional(),
heightMm: z.number().int().min(200).max(3000).optional(),
columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).default(2)
}).superRefine((value, context) => {
if (value.size === 'custom' && (!value.widthMm || !value.heightMm)) {
context.addIssue({ code: 'custom', message: 'Custom posters need a width and height in millimetres' });
}
});

export type PosterOptions = z.infer<typeof posterOptionsSchema>;

export function isPosterTheme(value: string): value is typeof posterThemes[number] {
return posterThemes.some((theme) => theme === value);
}

export function posterDimensions(options: PosterOptions): { widthMm: number; heightMm: number } {
if (options.size === 'custom') return { widthMm: options.widthMm!, heightMm: options.heightMm! };
const short = options.size === 'A0' ? 841 : 594;
const long = options.size === 'A0' ? 1189 : 841;
return options.orientation === 'landscape'
? { widthMm: long, heightMm: short }
: { widthMm: short, heightMm: long };
}

function texEscape(value: string): string {
const chars: Record<string, string> = {
'\\': '\\textbackslash{}', '{': '\\{', '}': '\\}', '%': '\\%', '$': '\\$',
'&': '\\&', '#': '\\#', '_': '\\_', '^': '\\textasciicircum{}', '~': '\\textasciitilde{}'
};
return value.replace(/\s+/g, ' ').replace(/[\\{}%$&#_^~]/g, (character) => chars[character]!);
}

export function posterFiles(theme: typeof posterThemes[number], title: string, input?: PosterOptions): Record<string, string> {
const options = posterOptionsSchema.parse(input ?? {});
const { widthMm, heightMm } = posterDimensions(options);
const source = posterTemplateFiles[theme]!;
const config = [
`% Poster settings. Edit this file to change the print size and metadata.`,
`\\def\\PosterWidth{${widthMm}}`,
`\\def\\PosterHeight{${heightMm}}`,
`\\def\\PosterTitle{${texEscape(title)}}`,
`\\def\\PosterSubtitle{${texEscape(options.subtitle)}}`,
`\\def\\PosterAuthors{${texEscape(options.authors)}}`,
`\\def\\PosterAffiliation{${texEscape(options.affiliation)}}`,
`\\def\\PosterContact{}`, `\\def\\PosterPaperURL{}`,
`\\def\\PosterColumns{${options.columns}}`,
`\\def\\PosterBodySize{27}`, `\\def\\PosterBodyLeading{35}`,
`\\def\\PosterHeadingSize{40}`, `\\def\\PosterTitleSize{68}`,
''
].join('\n');
return { ...source, 'config.tex': config, 'OVERPOSTER-LICENSE.txt': posterLicense };
}
Loading
Loading