mudancinhas
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 2m12s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 3m41s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 8s
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 2m12s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 3m41s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 8s
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -7,12 +7,15 @@ Leia o arquivo `project-context.md`, pois ele é a principal fonte de verdade do
|
||||
- Sempre que houver alteração em arquitetura, endpoints, contratos de API, variáveis de ambiente, UI/UX, fluxos de produto ou decisões técnicas, atualize o `project-context.md` na mesma entrega.
|
||||
|
||||
## Especificações de UI
|
||||
- Toda a interface precisa ter aparência de vidro, com um fundo não tão escuro.
|
||||
- Os botões precisam parecer modernos, no estilo iOS.
|
||||
- Todos os campos de texto, exibições e conteúdos devem ter uma tipografia moderna.
|
||||
- Todos os textos precisam estar em português brasileiro.
|
||||
- Toda a interface precisa ser implementada com código pronto para produção.
|
||||
- Não serão tolerados bugs na interface, muito menos problemas em CSS.
|
||||
- Siga TODAS as guidelines descritas no arquivo `GUIDELINES-ESTILO.md`
|
||||
- Se há alguma coisa que não ficou clara, pergunte.
|
||||
|
||||
## Testes
|
||||
- Não deve haver nenhuma forma de teste de integração, nem teste unitário, nem qualquer outro tipo de teste.
|
||||
- O único tipo de validação que deve ser feito é compilar o projeto - basta verificar se ele compila.
|
||||
- Toda a validação será feita por um humano depois.
|
||||
|
||||
## Compilação
|
||||
- Execute `.\re-build.ps1` para compilar o projeto. Os projetos serão compilados no Docker.
|
||||
|
||||
665
GUIDELINES-ESTILO.md
Normal file
665
GUIDELINES-ESTILO.md
Normal file
@@ -0,0 +1,665 @@
|
||||
# Diretrizes de Estilo do Mindforge
|
||||
|
||||
Diretrizes visuais da página de revisão de flashcards: usabilidade moderna com toques acadêmicos vintage — mesa de estudo pessoal, quente, tátil e iluminada.
|
||||
|
||||
---
|
||||
|
||||
## 1. Identidade do Produto
|
||||
|
||||
- **Nome**: Mindforge
|
||||
- **Conceito**: Estrutura moderna (grade limpa, vidro translúcido, cartões arredondados) + dicas vintage (papel, serifa, cores pergaminho, carimbo, ficha pautada).
|
||||
- **Tom**: Ambiente de biblioteca com legibilidade de aplicativo moderno. Sempre priorizar foco e UX para estudo.
|
||||
|
||||
---
|
||||
|
||||
## 2. Princípios de Design
|
||||
|
||||
1. **Claro e quente**: Fundos creme/pergaminho/dourado/azul desbotado. Sem tema escuro.
|
||||
2. **Vintage ao redor, legível no centro**: Serifa na marca e cabeçalhos; sem serifa no conteúdo do flashcard.
|
||||
3. **Sem animações contínuas**: Progresso estático, sem pulsos, sem cintilações. Movimento apenas para feedback direto (virar, carimbar, celebrar).
|
||||
4. **Hierarquia forte**: Flashcard é o ponto focal. Sidebar, topbar e painéis são suporte visual mais leve.
|
||||
5. **Tátil e limpo**: Texturas de papel, bordas tracejadas, sombras suaves. Evitar grunge pesado ou baixo contraste.
|
||||
|
||||
---
|
||||
|
||||
## 3. Variáveis CSS
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg: #fbf3df;
|
||||
--bg-soft: #fff9ea;
|
||||
--paper: #fffaf0;
|
||||
--paper-deep: #f2dfb3;
|
||||
--ink: #2b241c;
|
||||
--muted: #7b6a50;
|
||||
--line: rgba(82, 60, 28, 0.18);
|
||||
|
||||
--blue: #3f7cac;
|
||||
--blue-deep: #255f8d;
|
||||
--green: #4f8f5a;
|
||||
--green-deep: #32683b;
|
||||
--red: #b75b4d;
|
||||
--red-deep: #8d3c32;
|
||||
--gold: #c79539;
|
||||
--violet: #7e65a8;
|
||||
|
||||
--shadow: 0 28px 70px rgba(58, 42, 17, 0.18);
|
||||
--card-shadow: 0 30px 90px rgba(76, 48, 12, 0.20);
|
||||
|
||||
--radius-xl: 32px;
|
||||
--radius-lg: 22px;
|
||||
--radius-md: 16px;
|
||||
|
||||
--ease: cubic-bezier(.2,.75,.2,1);
|
||||
}
|
||||
```
|
||||
|
||||
| Token | Valor | Função |
|
||||
| --- | --- | --- |
|
||||
| `--bg` | `#fbf3df` | Fundo geral |
|
||||
| `--bg-soft` | `#fff9ea` | Superfícies claras |
|
||||
| `--paper` | `#fffaf0` | Face do cartão |
|
||||
| `--paper-deep` | `#f2dfb3` | Pergaminho profundo |
|
||||
| `--ink` | `#2b241c` | Texto principal |
|
||||
| `--muted` | `#7b6a50` | Texto secundário |
|
||||
| `--line` | `rgba(82, 60, 28, 0.18)` | Bordas/divisores |
|
||||
| `--blue` / `--blue-deep` | `#3f7cac` / `#255f8d` | Destaque, progresso, nota |
|
||||
| `--green` / `--green-deep` | `#4f8f5a` / `#32683b` | Correto, status ativo |
|
||||
| `--red` / `--red-deep` | `#b75b4d` / `#8d3c32` | Incorreto, difícil |
|
||||
| `--gold` | `#c79539` | Destaque vintage, médio |
|
||||
| `--violet` | `#7e65a8` | Celebração/destaque secundário |
|
||||
|
||||
Sombras são marrom-quentes (nunca preto neutro). Raio de borda: `--radius-xl` (contêineres grandes), `--radius-lg` (cartões médios), `--radius-md` (itens compactos). Easing: `cubic-bezier(.2,.75,.2,1)`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Estilo Global
|
||||
|
||||
```css
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 221, 146, .8), transparent 35%),
|
||||
radial-gradient(circle at 85% 15%, rgba(99, 153, 188, .22), transparent 34%),
|
||||
linear-gradient(135deg, #fff8e6 0%, #f9edcc 50%, #fbf5e7 100%);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(92, 68, 30, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(92, 68, 30, 0.035) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
mask-image: linear-gradient(to bottom, rgba(0,0,0,.65), rgba(0,0,0,.12));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Tipografia
|
||||
|
||||
- **UI global**: `Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`
|
||||
- **Marca e cabeçalhos**: `Georgia, "Times New Roman", serif`
|
||||
- **Conteúdo do flashcard (pergunta/resposta)**: `"Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif`
|
||||
|
||||
Cabeçalho da pergunta: `font-size: clamp(28px, 4.5vw, 46px)`, `line-height: 1.16`, `letter-spacing: -.035em`, `font-weight: 850`.
|
||||
Cabeçalho da resposta: `font-size: clamp(24px, 3.7vw, 36px)`, `line-height: 1.24`.
|
||||
Texto de apoio: `max-width: 590px`, `color: #66543d`, `font-size: 17px`, `line-height: 1.68`.
|
||||
|
||||
Metadados (rótulos, tags, estatísticas): `text-transform: uppercase`, `letter-spacing: .10em a .14em`, `font-weight: 900 a 950`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Layout
|
||||
|
||||
```css
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 288px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.main {
|
||||
min-width: 0;
|
||||
padding: 22px 28px 38px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 330px;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Barra Lateral
|
||||
|
||||
```css
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
padding: 24px 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 249, 234, .9), rgba(245, 226, 183, .84)),
|
||||
repeating-linear-gradient(45deg, rgba(113, 74, 18, .04) 0 1px, transparent 1px 8px);
|
||||
border-right: 1px solid var(--line);
|
||||
box-shadow: 16px 0 40px rgba(80, 54, 18, .08);
|
||||
z-index: 2;
|
||||
}
|
||||
```
|
||||
|
||||
### Marca
|
||||
|
||||
```css
|
||||
.brand { display: flex; align-items: center; gap: 14px; padding: 12px 12px 24px; }
|
||||
|
||||
.brand-mark {
|
||||
width: 46px; height: 46px;
|
||||
display: grid; place-items: center;
|
||||
border-radius: 15px;
|
||||
background: linear-gradient(135deg, #f2cf82, #fff2c8);
|
||||
border: 1px solid rgba(105, 73, 21, .18);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.7), 0 10px 22px rgba(129, 86, 27, .17);
|
||||
font-family: Georgia, serif; font-size: 22px; font-weight: 800; color: #51340d;
|
||||
}
|
||||
|
||||
.brand h1 { font-family: Georgia, "Times New Roman", serif; font-size: 22px; letter-spacing: -.03em; }
|
||||
```
|
||||
|
||||
### Navegação
|
||||
|
||||
```css
|
||||
.nav-section-title {
|
||||
margin: 20px 12px 10px;
|
||||
color: var(--muted); font-size: 11px; font-weight: 900;
|
||||
letter-spacing: .14em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative; width: 100%;
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 13px 13px; margin: 4px 0;
|
||||
border: 1px solid transparent; border-radius: 16px;
|
||||
color: #4b3b27; text-decoration: none;
|
||||
font-weight: 800; font-size: 14px;
|
||||
transition: .25s var(--ease); cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover { transform: translateX(4px); background: rgba(255,255,255,.45); border-color: var(--line); }
|
||||
|
||||
.nav-item.active {
|
||||
background: #fff7df; border-color: rgba(129, 86, 27, .16);
|
||||
box-shadow: 0 12px 28px rgba(104, 65, 14, .10);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 30px; height: 30px;
|
||||
display: grid; place-items: center;
|
||||
border-radius: 11px;
|
||||
background: rgba(255,255,255,.64);
|
||||
border: 1px solid rgba(80, 54, 18, .10);
|
||||
}
|
||||
```
|
||||
|
||||
### Cartão do baralho e progresso
|
||||
|
||||
```css
|
||||
.deck-card {
|
||||
margin-top: 24px; padding: 16px;
|
||||
border-radius: 22px;
|
||||
background: linear-gradient(145deg, rgba(255,255,255,.62), rgba(255,238,193,.70));
|
||||
border: 1px solid rgba(105, 73, 21, .16);
|
||||
box-shadow: 0 18px 40px rgba(82, 55, 18, .10);
|
||||
}
|
||||
|
||||
.mini-progress {
|
||||
height: 10px; border-radius: 999px; overflow: hidden;
|
||||
background: rgba(95, 72, 35, .14);
|
||||
}
|
||||
.mini-progress span { width: 54%; background: linear-gradient(90deg, var(--blue), #79a9c8, var(--gold)); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Barra Superior
|
||||
|
||||
```css
|
||||
.topbar {
|
||||
min-height: 72px;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 18px;
|
||||
padding: 14px 18px; margin-bottom: 24px;
|
||||
background: rgba(255, 250, 239, .72);
|
||||
border: 1px solid rgba(104, 69, 22, .13); border-radius: 26px;
|
||||
backdrop-filter: blur(18px);
|
||||
box-shadow: 0 18px 50px rgba(86, 57, 17, .08);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--blue-deep); font-size: 12px; font-weight: 950;
|
||||
letter-spacing: .14em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 4px rgba(79, 143, 90, .16);
|
||||
/* ESTÁTICO - nunca pulsar */
|
||||
}
|
||||
|
||||
.topbar h2 {
|
||||
margin: 0;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(24px, 3vw, 34px); letter-spacing: -.04em;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chip, .icon-button {
|
||||
min-height: 42px;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 9px;
|
||||
padding: 0 14px; border-radius: 999px;
|
||||
border: 1px solid rgba(96, 67, 28, .15);
|
||||
background: rgba(255,255,255,.62);
|
||||
color: #4b3b27; font-weight: 850; font-size: 13px;
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
|
||||
}
|
||||
|
||||
.icon-button:hover { transform: translateY(-2px) rotate(-2deg); box-shadow: 0 10px 20px rgba(86, 57, 17, .12); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Painel de Revisão
|
||||
|
||||
```css
|
||||
.review-panel {
|
||||
position: relative;
|
||||
min-height: 650px;
|
||||
padding: clamp(18px, 3vw, 34px);
|
||||
border-radius: var(--radius-xl);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,240,202,.46)),
|
||||
radial-gradient(circle at 15% 20%, rgba(255, 201, 101, .25), transparent 35%),
|
||||
radial-gradient(circle at 90% 10%, rgba(63, 124, 172, .16), transparent 32%);
|
||||
border: 1px solid rgba(104, 69, 22, .13);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.review-panel::before {
|
||||
width: 220px; height: 220px;
|
||||
right: -92px; top: 100px;
|
||||
background: rgba(63, 124, 172, .13);
|
||||
/* forma decorativa - nunca cobrir conteúdo */
|
||||
}
|
||||
|
||||
.review-panel::after {
|
||||
width: 180px; height: 180px;
|
||||
left: -70px; bottom: 60px;
|
||||
background: rgba(199, 149, 57, .18);
|
||||
}
|
||||
|
||||
.review-panel::before, .review-panel::after {
|
||||
position: absolute; border-radius: 999px; pointer-events: none; filter: blur(2px); opacity: .5;
|
||||
}
|
||||
|
||||
.session-header {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.session-title h3 { font-family: Georgia, serif; font-size: clamp(24px, 3vw, 36px); letter-spacing: -.04em; }
|
||||
.session-title p { max-width: 660px; color: var(--muted); font-size: 15px; line-height: 1.55; }
|
||||
|
||||
.score-pill {
|
||||
min-width: 76px; padding: 11px 12px; border-radius: 18px;
|
||||
background: rgba(255,255,255,.55); border: 1px solid rgba(100, 65, 18, .12);
|
||||
text-align: center; box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
|
||||
}
|
||||
.score-pill b { display: block; font-size: 19px; }
|
||||
.score-pill span { color: var(--muted); font-size: 10px; font-weight: 950; letter-spacing: .12em; text-transform: uppercase; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Flashcard
|
||||
|
||||
```css
|
||||
.stage {
|
||||
position: relative; z-index: 1;
|
||||
display: grid; place-items: center;
|
||||
min-height: 390px;
|
||||
perspective: 1400px;
|
||||
padding: 22px 0;
|
||||
}
|
||||
|
||||
.flashcard {
|
||||
position: relative;
|
||||
width: min(680px, 100%); min-height: 355px;
|
||||
cursor: pointer;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform .78s var(--ease), filter .35s var(--ease);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.flashcard:hover .card-face { border-color: rgba(63, 124, 172, .28); }
|
||||
.flashcard.is-flipped { transform: rotateY(180deg); }
|
||||
.flashcard.is-reviewed { animation: cardExit .58s var(--ease); }
|
||||
|
||||
.card-face {
|
||||
position: absolute; inset: 0; min-height: 355px;
|
||||
display: flex; flex-direction: column; justify-content: space-between;
|
||||
padding: clamp(24px, 5vw, 42px);
|
||||
border-radius: 30px;
|
||||
backface-visibility: hidden;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(168, 111, 36, .09) 0 1px, transparent 1px 22px),
|
||||
linear-gradient(rgba(168, 111, 36, .08) 0 1px, transparent 1px 30px),
|
||||
linear-gradient(145deg, #fffaf0, #f5dfaa);
|
||||
border: 1px solid rgba(82, 54, 17, .18);
|
||||
box-shadow: var(--card-shadow), inset 0 0 0 8px rgba(255,255,255,.24);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Borda tracejada de arquivo */
|
||||
.card-face::before {
|
||||
content: ""; position: absolute; inset: 18px;
|
||||
border: 1px dashed rgba(82, 54, 17, .18); border-radius: 22px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Brilho decorativo */
|
||||
.card-face::after {
|
||||
content: ""; position: absolute;
|
||||
width: 140px; height: 140px;
|
||||
right: -46px; bottom: -50px; border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(199,149,57,.25), transparent 66%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Verso: mais frio/azulado para diferenciar */
|
||||
.card-back {
|
||||
transform: rotateY(180deg);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(63,124,172,.08) 0 1px, transparent 1px 22px),
|
||||
linear-gradient(rgba(63,124,172,.07) 0 1px, transparent 1px 30px),
|
||||
linear-gradient(145deg, #fffaf1, #dfeef2);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; justify-content: space-between; gap: 12px; align-items: center;
|
||||
color: var(--muted); font-size: 12px; font-weight: 950;
|
||||
letter-spacing: .12em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 9px 12px; border-radius: 999px;
|
||||
background: rgba(255,255,255,.54); border: 1px solid rgba(82, 54, 17, .12);
|
||||
}
|
||||
|
||||
.card-question, .card-answer {
|
||||
position: relative; z-index: 1;
|
||||
display: grid; align-content: center; gap: 12px;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; justify-content: space-between; gap: 14px; align-items: center;
|
||||
color: var(--muted); font-size: 13px; font-weight: 800;
|
||||
}
|
||||
|
||||
.spacebar {
|
||||
padding: 4px 9px; border-radius: 8px;
|
||||
color: #4f3a1d; background: rgba(255,255,255,.58);
|
||||
border: 1px solid rgba(82, 54, 17, .14);
|
||||
box-shadow: inset 0 -2px 0 rgba(82, 54, 17, .08);
|
||||
font-size: 11px; font-weight: 950; letter-spacing: .08em; text-transform: uppercase;
|
||||
}
|
||||
|
||||
@keyframes cardExit {
|
||||
0% { transform: translateX(0) rotateY(180deg) rotateZ(0); opacity: 1; }
|
||||
45% { transform: translateX(28px) rotateY(180deg) rotateZ(2deg); opacity: .9; }
|
||||
100% { transform: translateX(-32px) rotateY(180deg) rotateZ(-2deg); opacity: 0; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Controles de Revisão
|
||||
|
||||
```css
|
||||
.controls {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; justify-content: center; gap: 16px; flex-wrap: wrap;
|
||||
opacity: .36; transform: translateY(10px);
|
||||
pointer-events: none;
|
||||
transition: .35s var(--ease);
|
||||
}
|
||||
|
||||
.controls.ready { opacity: 1; transform: translateY(0); pointer-events: auto; }
|
||||
|
||||
.review-button {
|
||||
position: relative;
|
||||
min-width: 170px; min-height: 60px;
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 10px;
|
||||
border: 0; border-radius: 20px;
|
||||
color: white; font-weight: 950; font-size: 16px; cursor: pointer;
|
||||
box-shadow: 0 16px 34px rgba(63, 44, 20, .18), inset 0 1px 0 rgba(255,255,255,.32);
|
||||
overflow: hidden;
|
||||
transition: .25s var(--ease);
|
||||
}
|
||||
|
||||
.review-button.correct { background: linear-gradient(135deg, var(--green), var(--green-deep)); }
|
||||
.review-button.wrong { background: linear-gradient(135deg, var(--red), var(--red-deep)); }
|
||||
|
||||
.review-button:hover { transform: translateY(-4px); box-shadow: 0 22px 42px rgba(63, 44, 20, .22), inset 0 1px 0 rgba(255,255,255,.32); }
|
||||
|
||||
/* Brilho no hover */
|
||||
.review-button::before {
|
||||
content: ""; position: absolute; inset: -90% -40%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent);
|
||||
transform: rotate(20deg) translateX(-80%);
|
||||
transition: .55s var(--ease);
|
||||
}
|
||||
.review-button:hover::before { transform: rotate(20deg) translateX(80%); }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Carimbo de Feedback
|
||||
|
||||
```css
|
||||
.stamp {
|
||||
position: absolute; right: 38px; top: 36px; z-index: 4;
|
||||
padding: 12px 18px;
|
||||
border: 4px double currentColor; border-radius: 10px;
|
||||
font-family: Georgia, serif; font-size: 26px; font-weight: 900;
|
||||
letter-spacing: .08em; text-transform: uppercase;
|
||||
opacity: 0; transform: rotate(-12deg) scale(1.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
.stamp.correct { color: var(--green-deep); }
|
||||
.stamp.wrong { color: var(--red-deep); }
|
||||
|
||||
.stamp.show { animation: stampIn .7s var(--ease); }
|
||||
|
||||
@keyframes stampIn {
|
||||
0% { opacity: 0; transform: rotate(-18deg) scale(1.8); }
|
||||
38% { opacity: 1; transform: rotate(-10deg) scale(.9); }
|
||||
58% { transform: rotate(-12deg) scale(1.04); }
|
||||
100% { opacity: 0; transform: rotate(-12deg) scale(1); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Painel Lateral Direito
|
||||
|
||||
```css
|
||||
.side-panel { display: grid; gap: 18px; }
|
||||
|
||||
.panel-card {
|
||||
padding: 20px; border-radius: 26px;
|
||||
background: rgba(255, 250, 239, .68);
|
||||
border: 1px solid rgba(104, 69, 22, .13);
|
||||
box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.panel-card h3 { margin: 0 0 14px; font-family: Georgia, serif; font-size: 22px; letter-spacing: -.03em; }
|
||||
|
||||
.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
|
||||
.stat {
|
||||
padding: 14px; border-radius: 18px;
|
||||
background: rgba(255,255,255,.52); border: 1px solid rgba(82, 54, 17, .10);
|
||||
}
|
||||
.stat b { display: block; font-size: 24px; letter-spacing: -.04em; }
|
||||
.stat span { color: var(--muted); font-size: 11px; font-weight: 950; letter-spacing: .10em; text-transform: uppercase; }
|
||||
|
||||
.track {
|
||||
height: 14px; border-radius: 999px; overflow: hidden;
|
||||
background: rgba(80, 54, 18, .12);
|
||||
}
|
||||
.track span { display: block; width: 42%; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--red), var(--gold), var(--green)); }
|
||||
|
||||
.queue { display: grid; gap: 10px; }
|
||||
|
||||
.queue-item {
|
||||
display: grid; grid-template-columns: 38px minmax(0, 1fr) auto;
|
||||
gap: 10px; align-items: center; padding: 10px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255,255,255,.48); border: 1px solid rgba(82, 54, 17, .10);
|
||||
}
|
||||
.queue-item strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.queue-number {
|
||||
width: 38px; height: 38px;
|
||||
display: grid; place-items: center;
|
||||
border-radius: 13px;
|
||||
background: #fff5d8; border: 1px solid rgba(82, 54, 17, .12);
|
||||
color: #74531c; font-family: Georgia, serif; font-weight: 900;
|
||||
}
|
||||
|
||||
.difficulty { width: 11px; height: 11px; border-radius: 50%; background: var(--gold); box-shadow: 0 0 0 5px rgba(199, 149, 57, .14); }
|
||||
.difficulty.easy { background: var(--green); box-shadow: 0 0 0 5px rgba(79, 143, 90, .14); }
|
||||
.difficulty.hard { background: var(--red); box-shadow: 0 0 0 5px rgba(183, 91, 77, .14); }
|
||||
|
||||
.note {
|
||||
position: relative;
|
||||
padding: 18px 18px 18px 22px; border-radius: 22px;
|
||||
background: linear-gradient(145deg, rgba(255, 246, 211, .9), rgba(255,255,255,.52));
|
||||
border: 1px solid rgba(104, 69, 22, .13);
|
||||
color: #5e4a2f; font-size: 14px; line-height: 1.55;
|
||||
}
|
||||
.note::before {
|
||||
content: ""; position: absolute;
|
||||
left: 10px; top: 16px; bottom: 16px; width: 4px; border-radius: 999px;
|
||||
background: var(--blue);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Animações
|
||||
|
||||
### Permitidas (curtas, acionadas pelo usuário)
|
||||
|
||||
| Animação | Gatilho | Duração |
|
||||
| --- | --- | --- |
|
||||
| Virada do flashcard (`rotateY(180deg)`) | Clique/Espaço/Enter | `.78s` |
|
||||
| Saída do cartão (`cardExit`) | Marcar Correto/Incorreto | `.58s` |
|
||||
| Carimbo (`stampIn`) | Marcar Correto/Incorreto | `.7s` |
|
||||
| Brilho do botão (hover) | Hover | `.55s` |
|
||||
| Confete (`confettiFly`) | Marcar Correto | `850ms` |
|
||||
| Revelação dos controles (`.controls.ready`) | Revelação da resposta | `.35s` |
|
||||
|
||||
### Proibidas
|
||||
|
||||
- Pulsos infinitos (pontos de status, progresso)
|
||||
- Barras de progresso animadas ou cintilantes
|
||||
- Movimento de fundo contínuo
|
||||
- Hover que transforme `.flashcard` conflitando com `rotateY`
|
||||
- Movimento decorativo durante leitura
|
||||
|
||||
---
|
||||
|
||||
## 15. Responsivo
|
||||
|
||||
### Tablet (`max-width: 1120px`)
|
||||
|
||||
```css
|
||||
@media (max-width: 1120px) {
|
||||
.app { grid-template-columns: 84px minmax(0, 1fr); }
|
||||
.sidebar { padding: 20px 10px; }
|
||||
.brand { justify-content: center; padding: 8px 0 18px; }
|
||||
.brand div:not(.brand-mark), .nav-section-title, .nav-text, .deck-card { display: none; }
|
||||
.nav-item { justify-content: center; padding: 12px; }
|
||||
.content-grid { grid-template-columns: 1fr; }
|
||||
.side-panel { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile (`max-width: 760px`)
|
||||
|
||||
```css
|
||||
@media (max-width: 760px) {
|
||||
.app { grid-template-columns: 1fr; }
|
||||
.sidebar { position: static; height: auto; display: flex; align-items: center; gap: 8px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.brand { min-width: 64px; padding: 0; }
|
||||
.nav-item { min-width: 56px; }
|
||||
.main { padding: 16px; }
|
||||
.topbar { align-items: flex-start; flex-direction: column; }
|
||||
.topbar h2 { white-space: normal; }
|
||||
.session-header { flex-direction: column; }
|
||||
.score-pills { justify-content: flex-start; }
|
||||
.review-panel { min-height: auto; }
|
||||
.stage { min-height: 420px; }
|
||||
.flashcard, .card-face { min-height: 380px; }
|
||||
.side-panel { grid-template-columns: 1fr; }
|
||||
.review-button { width: 100%; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Interação
|
||||
|
||||
- Revelar cartão: clique no flashcard, `Espaço` ou `Enter`. Adiciona `.is-flipped` e `.controls.ready`.
|
||||
- Botões inativos antes da revelação. Após: `Correto` (gradiente verde, carimbo verde + confete), `Incorreto` (gradiente vermelho, carimbo vermelho).
|
||||
- Atalhos: `Espaço` (revelar), `C` (correto), `W` (incorreto).
|
||||
|
||||
---
|
||||
|
||||
## 17. Acessibilidade
|
||||
|
||||
- Texto principal em tinta escura quente (`--ink`), nunca preto puro.
|
||||
- Conteúdo do flashcard sempre sem serifa legível.
|
||||
- Sem animações em loop. Movimento apenas sob ação do usuário.
|
||||
- Flashcard alcançável por teclado.
|
||||
- Flashcard é o ponto focal; painéis secundários usam menor contraste e fontes menores.
|
||||
|
||||
---
|
||||
|
||||
## 18. O que Fazer e Não Fazer
|
||||
|
||||
**Fazer**: creme quente, pergaminho, dourado, azul atenuado; página iluminada; serifa na marca e cabeçalhos; sem serifa no flashcard; linhas de papel e bordas tracejadas; progresso estático; animações curtas para feedback; espaçamento generoso.
|
||||
|
||||
**Não fazer**: tema escuro; pulsos ou cintilações infinitas; animar barras de progresso; hover no `.flashcard` que conflite com `rotateY`; fontes decorativas no conteúdo; opacidade alta na textura; sidebar/topbar mais fortes que o cartão; cores fora da paleta.
|
||||
@@ -5,9 +5,11 @@ ARG TARGETOS
|
||||
|
||||
WORKDIR /app
|
||||
COPY Mindforge.API.csproj ./
|
||||
RUN dotnet restore -a $TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then DOTNET_ARCH="x64"; else DOTNET_ARCH="$TARGETARCH"; fi && \
|
||||
dotnet restore -a "$DOTNET_ARCH"
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -a $TARGETARCH --no-restore -o /app/publish
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then DOTNET_ARCH="x64"; else DOTNET_ARCH="$TARGETARCH"; fi && \
|
||||
dotnet publish -c Release -a "$DOTNET_ARCH" --no-restore -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
|
||||
WORKDIR /app
|
||||
|
||||
@@ -10,14 +10,13 @@ namespace Mindforge.API.Providers
|
||||
{
|
||||
public class OpenAIApiProvider : ILlmApiProvider
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<OpenAIApiProvider> _logger;
|
||||
|
||||
public OpenAIApiProvider(HttpClient httpClient, IConfiguration configuration, ILogger<OpenAIApiProvider> logger)
|
||||
public OpenAIApiProvider(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<OpenAIApiProvider> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_httpClient.Timeout = TimeSpan.FromMinutes(5);
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -52,6 +51,9 @@ namespace Mindforge.API.Providers
|
||||
|
||||
Exception? lastErr = null;
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient();
|
||||
httpClient.Timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
@@ -60,7 +62,7 @@ namespace Mindforge.API.Providers
|
||||
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Mindforge.API.Services
|
||||
{
|
||||
public class GiteaService : IGiteaService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly string _baseUrl = string.Empty;
|
||||
private readonly string _owner = string.Empty;
|
||||
private readonly string _repo = string.Empty;
|
||||
@@ -21,9 +21,9 @@ namespace Mindforge.API.Services
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public GiteaService(HttpClient httpClient, IConfiguration configuration)
|
||||
public GiteaService(IHttpClientFactory httpClientFactory, IConfiguration configuration)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
|
||||
var repoUrl = configuration["GITEA_REPO_URL"];
|
||||
var token = configuration["GITEA_ACCESS_TOKEN"];
|
||||
@@ -78,7 +78,8 @@ namespace Mindforge.API.Services
|
||||
$"{_baseUrl}/api/v1/repos/{_owner}/{_repo}/raw/{path}?ref=master");
|
||||
request.Headers.Add("Authorization", $"token {_token}");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var response = await httpClient.SendAsync(request);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
throw new UserException($"File not found in repository: {path}");
|
||||
|
||||
@@ -90,9 +91,10 @@ namespace Mindforge.API.Services
|
||||
{
|
||||
EnsureConfigured();
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{endpoint}");
|
||||
request.Headers.Add("Authorization", $"token {_token}");
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var response = await httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="pt-BR">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
7
Mindforge.Web/package-lock.json
generated
7
Mindforge.Web/package-lock.json
generated
@@ -8,7 +8,6 @@
|
||||
"name": "mindforge-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@types/marked": "^5.0.2",
|
||||
"diff": "^8.0.3",
|
||||
"marked": "^17.0.4",
|
||||
"preact": "^10.29.0"
|
||||
@@ -849,12 +848,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/marked": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
|
||||
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/marked": "^5.0.2",
|
||||
"diff": "^8.0.3",
|
||||
"marked": "^17.0.4",
|
||||
"preact": "^10.29.0"
|
||||
|
||||
@@ -313,49 +313,6 @@ export function SpacedReviewComponent() {
|
||||
}
|
||||
};
|
||||
|
||||
const startSession = async () => {
|
||||
if (selectedStatuses.length === 0) {
|
||||
setError('Selecione ao menos um status para iniciar a revisao.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedLibraryIds.length === 0) {
|
||||
setError('Selecione ao menos um arquivo para iniciar a revisao.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRagLibraries.length === 0) {
|
||||
setError('Nenhum arquivo encontrado com os filtros atuais. Ajuste os status ou os arquivos selecionados.');
|
||||
return;
|
||||
}
|
||||
|
||||
setStartingSession(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library]));
|
||||
const libraryIds = Array.from(new Set(selectedRagLibraries.map((library) => library.libraryId)));
|
||||
const response = await MindforgeApiService.createFlashcardReviewSession({ libraryIds });
|
||||
const allowedLibraryIds = new Set(libraryIds);
|
||||
const filteredCards = response.cards.filter((card) => allowedLibraryIds.has(card.libraryId));
|
||||
const orderedCards = orderCardsForSession(filteredCards, ragByLibraryId);
|
||||
|
||||
if (orderedCards.length === 0) {
|
||||
setError('Os filtros selecionados nao retornaram cards para revisar.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionLibraries(selectedRagLibraries);
|
||||
setSessionCards(orderedCards);
|
||||
setCurrentIndex(0);
|
||||
setShowAnswer(false);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Falha ao iniciar revisao espacada.');
|
||||
} finally {
|
||||
setStartingSession(false);
|
||||
}
|
||||
};
|
||||
|
||||
const endSession = () => {
|
||||
setSessionCards([]);
|
||||
setSessionLibraries([]);
|
||||
|
||||
46
docker-bake.hcl
Normal file
46
docker-bake.hcl
Normal file
@@ -0,0 +1,46 @@
|
||||
group "default" {
|
||||
targets = ["web", "api", "cronjob"]
|
||||
}
|
||||
|
||||
group "release" {
|
||||
targets = ["web-release", "api-release", "cronjob-release"]
|
||||
}
|
||||
|
||||
target "web" {
|
||||
context = "./Mindforge.Web"
|
||||
dockerfile = "Dockerfile"
|
||||
tags = ["git.ivanch.me/ivanch/mindforge-web:latest"]
|
||||
args = {
|
||||
VITE_API_BASE_URL = "http://api.mindforge.haven"
|
||||
}
|
||||
}
|
||||
|
||||
target "web-release" {
|
||||
inherits = ["web"]
|
||||
platforms = ["linux/amd64", "linux/arm64"]
|
||||
output = ["type=registry"]
|
||||
}
|
||||
|
||||
target "api" {
|
||||
context = "./Mindforge.API"
|
||||
dockerfile = "Dockerfile"
|
||||
tags = ["git.ivanch.me/ivanch/mindforge-api:latest"]
|
||||
}
|
||||
|
||||
target "api-release" {
|
||||
inherits = ["api"]
|
||||
platforms = ["linux/amd64", "linux/arm64"]
|
||||
output = ["type=registry"]
|
||||
}
|
||||
|
||||
target "cronjob" {
|
||||
context = "./mindforge.cronjob"
|
||||
dockerfile = "Dockerfile"
|
||||
tags = ["git.ivanch.me/ivanch/mindforge-cronjob:latest"]
|
||||
}
|
||||
|
||||
target "cronjob-release" {
|
||||
inherits = ["cronjob"]
|
||||
platforms = ["linux/amd64", "linux/arm64"]
|
||||
output = ["type=registry"]
|
||||
}
|
||||
@@ -18,6 +18,8 @@ A interface é em **português brasileiro** e possui um tema escuro com efeito v
|
||||
| **TypeScript** | ~5.9.3 | Tipagem estática |
|
||||
| **marked** | ^17.0.4 | Renderização Markdown -> HTML |
|
||||
| **diff** | ^8.0.3 | Diff de texto (word-level) |
|
||||
|
||||
> Nota: `marked` v17+ inclui tipos TypeScript nativos. `@types/marked` foi removido. `@types/diff` v7 permanece em devDependencies como fallback de tipos.
|
||||
| **Google Fonts (Lato)** | 300/400/700 | Tipografia da interface |
|
||||
|
||||
### Backend API (Mindforge.API)
|
||||
@@ -130,7 +132,7 @@ Formas de requisição principais:
|
||||
### Backend (C#/.NET 9)
|
||||
- **Namespaces**: `Mindforge.API.Controllers`, `Mindforge.API.Services`, etc.
|
||||
- **Interfaces**: Prefixo `I` (ex.: `IFileService`, `ILlmApiProvider`).
|
||||
- **DI**: Todos os serviços registrados como `Scoped` em `Program.cs`.
|
||||
- **DI**: Todos os serviços registrados como `Scoped` em `Program.cs`. `HttpClient` obtido via `IHttpClientFactory` (não injetado diretamente).
|
||||
- **Controllers**: Atributo `[Route("api/v1/...")]`. Métodos retornam `IActionResult`.
|
||||
- **Tratamento de erros**: `ExceptionHandlingMiddleware` captura `UserException` (400) e exceções genéricas (500).
|
||||
|
||||
@@ -153,9 +155,13 @@ Formas de requisição principais:
|
||||
--font-main: 'Lato', sans-serif;
|
||||
```
|
||||
|
||||
### Git
|
||||
### Git e Scripts de Build/Deploy
|
||||
- Branches: `main` (produção)
|
||||
- CI/CD: Gitea Actions dispara no push para `main`, faz build com Docker Buildx multi-arch, envia para o Gitea Container Registry e faz o deploy no K8s.
|
||||
- Build Local: `re-build.ps1` detecta Docker local; se ausente, envia o projeto via SSH para `iris.haven` e executa `docker buildx bake --load` remotamente. Se Docker CLI presente, usa `DOCKER_HOST` para fallback remoto.
|
||||
- Deploy Local: `re-deploy.ps1` executa o build das imagens e aplica as atualizações no cluster Kubernetes.
|
||||
- API Dockerfile: mapeia `TARGETARCH=amd64` para `x64` (arquitetura esperada pelo .NET).
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
182
re-build.ps1
Normal file
182
re-build.ps1
Normal file
@@ -0,0 +1,182 @@
|
||||
# Configura para interromper a execucao em caso de qualquer erro
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$remoteHost = "root@iris.haven"
|
||||
$remoteBuildDir = "/tmp/mindforge-build"
|
||||
|
||||
# --- Funcoes auxiliares ---
|
||||
|
||||
function Test-SshConnection {
|
||||
param([string]$HostName)
|
||||
try {
|
||||
$null = ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes $HostName "echo ok" 2>$null
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-RemoteBuild {
|
||||
Write-Host "Preparando build remoto no host $remoteHost..." -ForegroundColor Cyan
|
||||
|
||||
# Cria diretorio temporario no remoto
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "rm -rf $remoteBuildDir && mkdir -p $remoteBuildDir"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha ao criar diretorio remoto."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Compacta o projeto (excluindo artefatos de build e arquivos desnecessarios)
|
||||
Write-Host "Compactando arquivos do projeto..."
|
||||
$tempTar = "$env:TEMP\mindforge-build.tar.gz"
|
||||
$excludes = @(
|
||||
"--exclude=.git",
|
||||
"--exclude=node_modules",
|
||||
"--exclude=bin",
|
||||
"--exclude=obj",
|
||||
"--exclude=.vs",
|
||||
"--exclude=.vscode",
|
||||
"--exclude=.cache",
|
||||
"--exclude=dist",
|
||||
"--exclude=.idea",
|
||||
"--exclude=*.user",
|
||||
"--exclude=appsettings.Development.json"
|
||||
)
|
||||
& tar czf $tempTar $excludes -C (Get-Location) .
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha ao compactar arquivos do projeto."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Envia o arquivo compactado para o host remoto
|
||||
Write-Host "Enviando arquivos do projeto para o host remoto..."
|
||||
scp -O -o StrictHostKeyChecking=no $tempTar "${remoteHost}:${remoteBuildDir}/project.tar.gz"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha ao enviar arquivos para o host remoto."
|
||||
Remove-Item -Force $tempTar -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Remove o arquivo temporario local
|
||||
Remove-Item -Force $tempTar -ErrorAction SilentlyContinue
|
||||
|
||||
# Extrai no host remoto
|
||||
Write-Host "Extraindo arquivos no host remoto..."
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "cd $remoteBuildDir && tar xzf project.tar.gz && rm project.tar.gz"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha ao extrair arquivos no host remoto."
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "rm -rf $remoteBuildDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Garante que o builder buildx existe no remoto
|
||||
Write-Host "Verificando builder Buildx no remoto..."
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "docker buildx inspect mindforge-builder 2>/dev/null || docker buildx create --name mindforge-builder --use"
|
||||
|
||||
# Executa o build no remoto
|
||||
Write-Host "Compilando as imagens Docker no host remoto..."
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "cd $remoteBuildDir && docker buildx bake --load"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Falha na compilacao remota."
|
||||
# Limpa diretorio remoto mesmo em caso de falha
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "rm -rf $remoteBuildDir"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Limpa diretorio remoto
|
||||
ssh -o StrictHostKeyChecking=no $remoteHost "rm -rf $remoteBuildDir"
|
||||
}
|
||||
|
||||
# --- Logica principal ---
|
||||
|
||||
# Verifica se o executavel 'docker' esta disponivel no PATH
|
||||
$dockerCliAvailable = $false
|
||||
if (Get-Command "docker" -ErrorAction SilentlyContinue) {
|
||||
$dockerCliAvailable = $true
|
||||
}
|
||||
|
||||
if ($dockerCliAvailable) {
|
||||
# --- Modo local (Docker CLI presente) ---
|
||||
|
||||
# Habilita o BuildKit para compilacoes com 'docker build' padrao
|
||||
$env:DOCKER_BUILDKIT = "1"
|
||||
|
||||
# Testa se o daemon do Docker local esta rodando
|
||||
$localDaemonRunning = $false
|
||||
try {
|
||||
$null = docker version --format '{{.Server.Version}}' 2>$null
|
||||
$localDaemonRunning = $true
|
||||
} catch {
|
||||
$localDaemonRunning = $false
|
||||
}
|
||||
|
||||
if (-not $localDaemonRunning) {
|
||||
Write-Host "O daemon local do Docker nao esta rodando. Redirecionando comandos para o host remoto $remoteHost..."
|
||||
$env:DOCKER_HOST = "ssh://$remoteHost"
|
||||
|
||||
# Testa a conexao com o host remoto
|
||||
try {
|
||||
$null = docker version --format '{{.Server.Version}}' 2>$null
|
||||
Write-Host "Conexao com o host remoto estabelecida com sucesso!"
|
||||
} catch {
|
||||
Write-Error "Nao foi possivel conectar ao Docker local nem ao host remoto $remoteHost. Certifique-se de que o Docker Desktop local esta iniciado ou que o servidor remoto esta acessivel."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Detecta se o Docker Buildx esta disponivel
|
||||
$buildxAvailable = $false
|
||||
try {
|
||||
$null = docker buildx version 2>$null
|
||||
$buildxAvailable = $true
|
||||
} catch {
|
||||
$buildxAvailable = $false
|
||||
}
|
||||
|
||||
if ($buildxAvailable) {
|
||||
# Garante que o builder padrao do buildx existe e esta ativo
|
||||
try {
|
||||
$null = docker buildx inspect mindforge-builder 2>$null
|
||||
Write-Host "Usando builder 'mindforge-builder' existente..."
|
||||
} catch {
|
||||
Write-Host "Criando e configurando builder 'mindforge-builder'..."
|
||||
docker buildx create --name mindforge-builder --use
|
||||
}
|
||||
|
||||
# Compila as imagens usando o docker buildx bake
|
||||
Write-Host "Compilando as imagens Docker usando o Buildx..."
|
||||
docker buildx bake --load
|
||||
} else {
|
||||
# Fallback: build tradicional sem buildx
|
||||
$env:DOCKER_BUILDKIT = "0"
|
||||
Write-Warning "O plugin Docker Buildx nao foi encontrado. Executando build padrao (single-arch) local para cada servico..."
|
||||
|
||||
# Detecta a arquitetura do sistema para passar como build-arg
|
||||
$sysArch = "$env:PROCESSOR_ARCHITECTURE".ToLower()
|
||||
$dockerArch = if ($sysArch -eq "amd64") { "amd64" } else { $sysArch }
|
||||
$buildArgs = "--build-arg TARGETARCH=$dockerArch --build-arg TARGETOS=linux --build-arg BUILDPLATFORM=linux/$dockerArch"
|
||||
|
||||
Write-Host "Compilando Mindforge.Web..."
|
||||
docker build -t git.ivanch.me/ivanch/mindforge-web:latest ./Mindforge.Web --build-arg VITE_API_BASE_URL=http://api.mindforge.haven
|
||||
|
||||
Write-Host "Compilando Mindforge.API..."
|
||||
docker build -t git.ivanch.me/ivanch/mindforge-api:latest ./Mindforge.API $buildArgs
|
||||
|
||||
Write-Host "Compilando mindforge.cronjob..."
|
||||
docker build -t git.ivanch.me/ivanch/mindforge-cronjob:latest ./mindforge.cronjob $buildArgs
|
||||
}
|
||||
} else {
|
||||
# --- Modo remoto (Docker CLI ausente localmente) ---
|
||||
Write-Host "Docker CLI nao encontrado localmente." -ForegroundColor Yellow
|
||||
|
||||
if (Test-SshConnection $remoteHost) {
|
||||
Write-Host "Conectando ao host remoto $remoteHost para build..."
|
||||
Invoke-RemoteBuild
|
||||
} else {
|
||||
Write-Error "Docker nao encontrado localmente e host remoto $remoteHost inacessivel. Instale o Docker ou verifique a conectividade SSH."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Compilacao concluida com sucesso." -ForegroundColor Green
|
||||
@@ -1,12 +0,0 @@
|
||||
# Exit on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
docker login git.ivanch.me
|
||||
|
||||
Write-Host "Building and pushing Docker images..."
|
||||
docker buildx bake --set '*.platform=linux/amd64,linux/arm64' --push
|
||||
|
||||
Write-Host "Build and push completed successfully."
|
||||
|
||||
kubectl delete -f ./deploy/deployment.yaml
|
||||
kubectl apply -f ./deploy/deployment.yaml
|
||||
Reference in New Issue
Block a user