feat(ybira): fase 2 — pastas, lixeira, paginação, download e cota - #151
Conversation
Implements Maraca.Behaviour: bootstrap (single-tekoa first boot), invite-by-link (no email dependency, RFC 002 D6), email confirmation, authentication, sessions, password reset, owner-first authorization with explicit permissions and admin access requests. - avas.username now nullable: invite flow sets it at confirmation - Permission.changeset no longer casts granted_by_id/tekoa_id (anti-spoofing — context fields set only via put_change) - destroy_session clears and drops the session (fixation-safe)
|
@coderabbitai review, leve em consideração a nova RFC do tekoa https://github.com/taina-labs/tekoa/blob/main/tecnico/RFC_002_MVP.md |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Fetch the RFC content
gh api repos/taina-labs/tekoa/contents/tecnico/RFC_002_MVP.md --jq '.content' | base64 -d 2>/dev/null || \
curl -s "https://raw.githubusercontent.com/taina-labs/tekoa/main/tecnico/RFC_002_MVP.md" 2>/dev/null || \
echo "Could not fetch RFC directly"
echo "=== PR FILES ==="
gh pr view 151 --repo taina-labs/taina --json files --jq '.files[].path' 2>/dev/null
echo "=== PR DIFF (truncated) ==="
gh pr diff 151 --repo taina-labs/taina 2>/dev/null | head -2000Length of output: 89694 🏁 Script executed: #!/bin/bash
echo "=== REMAINING DIFF ==="
gh pr diff 151 --repo taina-labs/taina 2>/dev/null | tail -n +2001 | head -2000
echo "=== Scope module ==="
fd "scope.ex" --exec cat {}
echo "=== Maraca Tekoa schema ==="
fd "tekoa.ex" --exec cat {}
echo "=== Ybira File schema (full) ==="
fd "file.ex" --exec grep -n "field\|schema\|tekoa_id\|deleted_at\|folder_id" {}
echo "=== Check Scope.for_ava usage ==="
rg "for_ava" --type elixir -n
echo "=== Check storage layout in copy_to_storage ==="
rg "copy_to_storage\|storage_root\|ano\|mes\|year\|month" lib/taina/ybira.ex -n -A3
echo "=== Check purge_deleted_files file rm order ==="
rg "purge_deleted\|File.rm\|Repo.delete" lib/taina/ybira.ex -n -A2
echo "=== ancestor_or_self recursion (depth guard?) ==="
rg "ancestor_or_self" lib/taina/ybira.ex -n -A5
echo "=== list_permissions skip_tekoa ==="
rg "list_permissions\|skip_tekoa" lib/taina/maraca.ex -n -A3
echo "=== Permission changeset - granted_by_id ==="
cat lib/taina/maraca/permission.ex | grep -A20 "def changeset"
echo "=== delete_folder soft delete (folders missing from purge?) ==="
rg "purge_deleted\|deleted_at.*folder" lib/taina/ybira.ex -n -A3
echo "=== Oban testing config ==="
cat config/test.exs
echo "=== Check Repo.exists? signature ==="
rg "def exists\?" lib/taina/repo.ex -n -A5
echo "=== Check Repo.transact ==="
rg "def transact\|transact(" lib/taina/repo.ex -n -A5Length of output: 38523
🔴 Crítico1.
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (35)
WalkthroughEste PR implementa a Fase 2 do Ybira com soft delete e pasta, integra Oban para jobs agendados, expande Maraca com novos contratos de autorização, e entrega download HTTP com suporte a Range. O fluxo central transforma o ciclo de vida de arquivos de hard delete para soft delete com trash/restore, adiciona CRUD completo de pastas com validação de ciclos, paginação por cursor, detecção MIME por magic bytes, e agendamento automático de purga a cada 30 dias via worker. Inclui controller autenticado para download com RFC 7233 (parciais), refresh da infraestrutura de teste, e migrations de schema com índices otimizados. Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/taina/ybira/file.ex (1)
102-134:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCampo programático
deleted_atestá sendo aceito viacast.O
deleted_até sempre definido programaticamente através dedelete_changeset/1erestore_changeset/1, nunca devendo vir de input externo. Incluí-lo nocast(linha 112) abre a possibilidade de attrs de usuário sobrescreverem o soft delete inadvertidamente.Que tal removê-lo do cast para reforçar que é estritamente programático?
🔧 Sugestão de ajuste
def changeset(file, attrs) do file |> cast(attrs, [ :filename, :original_filename, :filepath, :mime_type, :file_size_bytes, :file_hash, :metadata, - :deleted_at, :ava_id, :tekoa_id, :folder_id ])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/taina/ybira/file.ex` around lines 102 - 134, Remove :deleted_at from the cast list in the changeset/2 function so external attrs cannot set the soft-delete flag; keep deleted_at managed only by the programmatic delete_changeset/1 and restore_changeset/1 helpers and ensure any existing references to :deleted_at remain only in those functions and not in changeset/2 or other user-input paths.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@config/config.exs`:
- Around line 14-20: Ajuste a entrada do cron em Oban para explicitar a fila do
job de PurgeTrash: edite o plugin {Oban.Plugins.Cron, crontab: [...] } e na
tupla crontab substitua a referência {"0 3 * * *",
Taina.Ybira.Workers.PurgeTrash} por {"0 3 * * *",
Taina.Ybira.Workers.PurgeTrash, queue: :default} (ou use queue: :maintenance se
quiser isolar tarefas de manutenção); isso garante que o worker
Taina.Ybira.Workers.PurgeTrash execute na fila desejada em vez de depender do
comportamento padrão.
In `@lib/taina_web/controllers/file_controller.ex`:
- Around line 71-80: A função suffix_range/2 pode retornar um last negativo
quando total == 0 (arquivo vazio) e um cliente pede bytes=-N; atualize
suffix_range(suffix, total) para validar total > 0 e garantir que o intervalo
calculado seja válido antes de retornar: ao parsear {len, ""} verifique primeiro
total > 0, calcule first = max(total - len, 0) e only return {:ok, first, total
- 1} se first <= total - 1; caso contrário (total == 0 ou first > total - 1)
retorne :error para evitar last negativo e problemas em send_file/2.
In `@lib/taina/maraca/unauthorized_error.ex`:
- Line 17: The string interpolation uses `ava && ava.public_id`, which can yield
"false" in edge cases; update the code to pattern-match `ava` explicitly and use
its `public_id` (e.g., match `%Ava{public_id: public_id}` in the function head
or a preceding `case`/`with`) and interpolate `public_id` instead of `ava &&
ava.public_id`; locate this change in the module/function that builds the
unauthorized message (referencing `ava.public_id` and the surrounding message
construction in UnauthorizedError) and ensure the fallback when `ava` is
nil/absent yields an empty or descriptive value rather than "false".
In `@lib/taina/repo.ex`:
- Around line 144-152: Add a debug log when an infrastructure table is detected
by infra_query?/1: inside the clause defp infra_query?(%Ecto.Query{from:
%{source: {table, _schema}}}) when is_binary(table) do, keep the existing
String.starts_with?(`@infra_table_prefixes`) check but, when true, call
Logger.debug with a concise message like "infra query permitida:
tabela=#{table}" (ensure require Logger is present); return true for the match
and false otherwise so behavior is unchanged while adding observability.
In `@lib/taina/ybira.ex`:
- Around line 499-505: apply_cursor currently swallows decode_cursor failures
and returns the original query, causing clients with invalid/expired cursors to
silently receive the first page; update apply_cursor (and call sites like
list_files/3) to surface invalid cursors instead of silently falling back:
change apply_cursor/2 to return an explicit error tuple (e.g., {:error,
:invalid_cursor}) when decode_cursor returns :error and update list_files/3 to
propagate that error to the caller (or alternatively log the invalid cursor and
return {:error, :invalid_cursor} from list_files/3) so clients receive a clear
failure instead of silent pagination reset.
- Around line 309-311: The call to Elixir.File.rm(file.filepath) in ybira.ex
currently ignores errors so orphaned files can accumulate silently; update the
cleanup to pattern-match the result of Elixir.File.rm and log failures via
Logger (e.g. Logger.warning) including the filepath and file.id (or other
identifier) and the error reason so disk/permission issues are visible and
traceable; ensure successful :ok still proceeds without raising.
- Around line 430-449: soft_delete_tree currently traverses folders recursively
(functions: soft_delete_tree, Repo.all selecting Ybira.Folder ids,
Repo.update_all on Ybira.File/Ybira.Folder) which issues many sequential queries
for deep trees; to fix, replace the recursive traversal with a single recursive
CTE that selects all descendant folder ids and then perform two bulk updates
(one on Ybira.File where folder_id IN CTE ids and one on Ybira.Folder where id
IN CTE ids) or alternatively offload the existing soft_delete_tree work to an
asynchronous worker (e.g., Oban/Task) so deletions for large trees run outside
the request and avoid timeouts; pick CTE for DB-side efficiency or worker for
deferred execution and update the soft_delete_tree function to call the chosen
approach.
- Around line 414-428: The ancestor_or_self?/2 recursion can blow the stack and
issue many DB queries for deep trees; change it to a depth-limited iterative
approach (or use a recursive CTE) to avoid deep recursion and N queries:
implement a loop in ancestor_or_self?/2 that queries parent_id iteratively up to
a MAX_DEPTH (e.g. 50) and return {:error, :too_deep} or false when exceeded, and
apply the same refactor to soft_delete_tree/1 (replace recursive calls with an
iterative traversal or single recursive CTE delete) so both functions no longer
recurse per level and enforce a clear depth limit or use a single-query
recursive CTE for large hierarchies.
In `@lib/taina/ybira/workers/purge_trash.ex`:
- Around line 19-23: O worker perform/1 em purge_trash.ex não expõe métricas ou
logs sobre a purga; ao chamar Ybira.purge_deleted_files(cutoff) capture o
retorno (atualmente {:ok, _purged}) e emita um log estruturado ou uma métrica
contendo pelo menos o número de arquivos removidos e bytes recuperados; por
exemplo, inspecione a estrutura retornada por purge_deleted_files (nome
identificador: _purged) para extrair counts e bytes, use Logger.info com
map/keyword ou dispare um evento Telemetry.emit/3 com esses campos, e mantenha a
função perform/1 retornando :ok após o envio do log/métrica.
In `@priv/repo/migrations/20260610120000_allow_pending_invite_username.exs`:
- Around line 10-13: Replace the single change/0 that uses execute/2 with
explicit up/0 and down/0 functions to make the migration intent clearer:
implement def up do execute "ALTER TABLE maraca.avas ALTER COLUMN username DROP
NOT NULL" end and def down do execute "ALTER TABLE maraca.avas ALTER COLUMN
username SET NOT NULL" end so the migration applies the DROP NOT NULL on up and
the SET NOT NULL on down (replace the existing change/0 implementation with
these two functions).
In `@test/taina/maraca_test.exs`:
- Around line 14-350: Extract the repeated token-expiration setup into a small
test helper (e.g. expire_token/3) and replace the duplicated DateTime.add +
Ecto.Changeset.change + Repo.update! sequences in the tests that expire tokens
(the blocks that change invited.email_confirmation_sent_at and
with_token.reset_token_sent_at) to call that helper; implement expire_token to
accept the struct (e.g. %Ava{}), the field atom (:email_confirmation_sent_at or
:reset_token_sent_at) and a tuple like {8, :day} or {2, :hour} (compute negative
duration with DateTime.add and update via Ecto.Changeset.change |>
Repo.update!()) so both tests use the helper for clarity and DRYness.
---
Outside diff comments:
In `@lib/taina/ybira/file.ex`:
- Around line 102-134: Remove :deleted_at from the cast list in the changeset/2
function so external attrs cannot set the soft-delete flag; keep deleted_at
managed only by the programmatic delete_changeset/1 and restore_changeset/1
helpers and ensure any existing references to :deleted_at remain only in those
functions and not in changeset/2 or other user-input paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: d890d136-b64d-4791-977a-0137ed5bc75b
⛔ Files ignored due to path filters (1)
mix.lockis excluded by!**/*.lock
📒 Files selected for processing (29)
config/config.exsconfig/test.exslib/taina/application.exlib/taina/maraca.exlib/taina/maraca/ava.exlib/taina/maraca/behaviour.exlib/taina/maraca/permission.exlib/taina/maraca/unauthorized_error.exlib/taina/repo.exlib/taina/ybira.exlib/taina/ybira/behaviour.exlib/taina/ybira/file.exlib/taina/ybira/folder.exlib/taina/ybira/mime_detector.exlib/taina/ybira/workers/purge_trash.exlib/taina_web/controllers/file_controller.exlib/taina_web/router.exmix.exspriv/repo/migrations/20260610120000_allow_pending_invite_username.exspriv/repo/migrations/20260610130000_setup_oban.exspriv/repo/migrations/20260610130001_ybira_fase2_indexes.exstest/support/conn_case.extest/support/fixtures.extest/taina/maraca_test.exstest/taina/ybira/mime_detector_test.exstest/taina/ybira/purge_trash_test.exstest/taina/ybira_test.exstest/taina_web/controllers/file_controller_test.exstest/test_helper.exs
…rga de pastas; download com content-disposition
|
@coderabbitai review the last commit, some comments were addressed, the ones not were skipped/deffered so you can safely resolve the comment |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Last commit info ==="
gh pr view 151 --repo taina-labs/taina --json commits --jq '.commits[-1]'
echo ""
echo "=== Last commit SHA ==="
LAST_SHA=$(gh pr view 151 --repo taina-labs/taina --json commits --jq '.commits[-1].oid')
echo "SHA: $LAST_SHA"
echo ""
echo "=== Last commit diff ==="
gh api repos/taina-labs/taina/commits/$LAST_SHA 2>/dev/null | jq -r '.files[] | "--- \(.filename)\n+++ \(.filename)\n" + (.patch // "")' 2>/dev/null | head -3000Length of output: 12824
✅ Resolvido neste commit
🔵 Observação residual (não bloqueante)CTEs usam
# ATENÇÃO: `Repo.query/2` bypassa o `prepare_query/3` (sem filtro tekoa
# automático). Seguro aqui porque `root_id`/`start_id` só chegam após
# verificação de propriedade no caller — não expor esse helper sem essa garantia.
defp descendant_folder_ids(root_id) do
⏭️ Deferred (confirmado ok para este PR)
Gostaria de abrir issues de acompanhamento para os itens deferidos? 🌱 [resolve] |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Get the last commit info
gh pr view 151 --repo taina-labs/taina --json commits --jq '.commits[-1]'
echo "=== Last commit diff ==="
gh api repos/taina-labs/taina/commits/$(gh pr view 151 --repo taina-labs/taina --json commits --jq '.commits[-1].oid') --jq '.files[].filename'
echo "=== Full diff of last commit ==="
LAST_SHA=$(gh pr view 151 --repo taina-labs/taina --json commits --jq '.commits[-1].oid')
gh api repos/taina-labs/taina/commits/$LAST_SHA 2>/dev/null | jq -r '.files[] | "=== \(.filename) ===\n\(.patch // "binary or no patch")"'
echo "=== My review comments on PR ==="
gh api repos/taina-labs/taina/pulls/151/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, path: .path, body: .body[0:200]}'
echo "=== My issue comments on PR ==="
gh api repos/taina-labs/taina/issues/151/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[0:300]}'Length of output: 17083 🏁 Script executed: #!/bin/bash
# Resolve the inline review comments that were addressed in the last commit
ADDRESSED_COMMENT_IDS=(3389244213 3389244256 3389244271 3389244284 3389244296)
for id in "${ADDRESSED_COMMENT_IDS[@]}"; do
echo "Resolving comment $id..."
# GitHub doesn't have a direct "resolve" API for PR review comments,
# but we can check if they're threads to resolve
# Let's get the thread/pull request review thread IDs
gh api repos/taina-labs/taina/pulls/151/comments/$id --jq '{id: .id, path: .path, body: .body[0:100]}'
done
echo "=== Fetching review threads to resolve ==="
gh api graphql -f query='
{
repository(owner: "taina-labs", name: "taina") {
pullRequest(number: 151) {
reviewThreads(first: 50) {
nodes {
id
isResolved
comments(first: 1) {
nodes {
databaseId
body
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {threadId: .id, commentId: .comments.nodes[0].databaseId, body: .comments.nodes[0].body[0:120]}'Length of output: 3197 🏁 Script executed: #!/bin/bash
# Resolve all remaining open threads (addressed + deferred/skipped by user)
THREAD_IDS=(
"PRRT_kwDOPnKwgs6IhVmy"
"PRRT_kwDOPnKwgs6IhVnR"
"PRRT_kwDOPnKwgs6IhVnX"
"PRRT_kwDOPnKwgs6IhVn3"
"PRRT_kwDOPnKwgs6IhVn-"
"PRRT_kwDOPnKwgs6IhVoG"
)
for thread_id in "${THREAD_IDS[@]}"; do
echo "Resolving thread $thread_id..."
gh api graphql -f query="
mutation {
resolveReviewThread(input: {threadId: \"$thread_id\"}) {
thread {
id
isResolved
}
}
}
" --jq '.data.resolveReviewThread.thread'
done
echo "=== Verification: remaining unresolved threads ==="
gh api graphql -f query='
{
repository(owner: "taina-labs", name: "taina") {
pullRequest(number: 151) {
reviewThreads(first: 50) {
nodes {
id
isResolved
comments(first: 1) {
nodes {
databaseId
body
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | {threadId: .id, commentId: .comments.nodes[0].databaseId}'Length of output: 1765
Aqui um resumo rápido do que o último commit endereçou:
Os itens adiados (Permission.changeset [resolve] |
|
@coderabbitai, sim, pode abrir as issues seguindo os templates do projeto e com linguagem clara/explicita, humanizada por favor. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Issue templates ==="
find . -path "*/.github/ISSUE_TEMPLATE*" -o -name "*.md" | grep -i "issue_template\|ISSUE_TEMPLATE" 2>/dev/null | head -20
ls .github/ISSUE_TEMPLATE/ 2>/dev/null || ls .github/ 2>/dev/null || echo "No .github dir found"
echo ""
echo "=== Template contents ==="
fd "ISSUE_TEMPLATE\|issue_template" --type f --exec cat {} \; 2>/dev/null
echo ""
echo "=== Existing open issues (for reference) ==="
gh issue list --repo taina-labs/taina --limit 10 --json number,title,labels
echo ""
echo "=== Available labels ==="
gh label list --repo taina-labs/taina --json name,description 2>/dev/null | jq -r '.[] | "\(.name): \(.description)"'Length of output: 749 🏁 Script executed: #!/bin/bash
echo "=== Issue templates via API ==="
gh api repos/taina-labs/taina/contents/.github/ISSUE_TEMPLATE 2>/dev/null | jq -r '.[].name' || echo "No ISSUE_TEMPLATE folder"
echo ""
echo "=== Try root .github ==="
gh api repos/taina-labs/taina/contents/.github 2>/dev/null | jq -r '.[].name'
echo ""
echo "=== Available labels ==="
gh label list --repo taina-labs/taina --json name,description 2>/dev/null | jq -r '.[] | "\(.name): \(.description)"'
echo ""
echo "=== Existing open issues (reference) ==="
gh issue list --repo taina-labs/taina --limit 10 --json number,title,labels --jq '.[] | "#\(.number) \(.title)"'Length of output: 1343 🏁 Script executed: #!/bin/bash
echo "=== Template: desenvolvimento.md ==="
gh api repos/taina-labs/taina/contents/.github/ISSUE_TEMPLATE/desenvolvimento.md --jq '.content' | base64 -d
echo ""
echo "=== Template: documentação.md ==="
gh api repos/taina-labs/taina/contents/.github/ISSUE_TEMPLATE/documentação.md --jq '.content' | base64 -dLength of output: 940 🏁 Script executed: #!/bin/bash
PR_URL="https://github.com/taina-labs/taina/pull/151"
# Issue 1: Permission.changeset granted_by_id auditoria
gh api repos/taina-labs/taina/issues \
--method POST \
--field title="[Maraca] Trilha de auditoria: garantir que granted_by_id nunca seja nil em Permission" \
--field body="**Descrição**
Atualmente, o \`Permission.changeset/2\` faz \`cast\` e \`validate_required\` apenas para \`:resource_id\`, \`:resource_type\`, \`:action\` e \`:ava_id\`. O campo \`granted_by_id\` — que representa **quem concedeu a permissão** — é injetado externamente via \`put_change/3\` por quem chama o changeset.
Isso funciona nos caminhos conhecidos (\`insert_grant\` e \`approve_access_request\`), mas deixa uma brecha silenciosa: qualquer novo caminho que chame \`Permission.changeset/2\` diretamente pode persistir uma permissão sem \`granted_by_id\`, comprometendo a trilha de auditoria sem levantar nenhum erro.
Num sistema orientado à soberania e transparência das comunidades, saber **quem** concedeu uma permissão é tão importante quanto saber **o que** foi concedido.
**Tarefas**
- [ ] Criar um \`grant_changeset/2\` específico que inclua \`granted_by_id\` e \`tekoa_id\` no \`cast\` e no \`validate_required\`
- [ ] Migrar \`insert_grant\` e \`approve_access_request\` para usar \`grant_changeset/2\`
- [ ] Adicionar teste cobrindo a rejeição de uma permissão sem \`granted_by_id\`
- [ ] Revisar se há outros call sites que chamam \`Permission.changeset\` diretamente
_Levantado durante a revisão do `#151` por `@zoedsoupe` — $PR_URL_" \
--field labels='["engineering"]' \
--field assignees='["zoedsoupe"]' \
--jq '"Created: #\(.number) — \(.title)\n\(.html_url)"'
echo ""
# Issue 2: list_trash/2 visibilidade admin
gh api repos/taina-labs/taina/issues \
--method POST \
--field title="[Ybira] Admins não conseguem ver a lixeira de outros membros da comunidade" \
--field body="**Descrição**
A função \`list_trash/2\` filtra os arquivos deletados pelo \`ava_id\` do scope, ou seja, cada usuário enxerga **apenas sua própria lixeira**. Isso é o comportamento correto para membros comuns, mas deixa o admin sem visibilidade sobre os arquivos deletados de toda a tekoa.
O problema prático: se a cota de armazenamento de uma comunidade está alta e o owner ou admin quer entender o motivo, não consegue auditar quais arquivos estão na lixeira ocupando espaço (pois os bytes não são devolvidos no soft delete — só no purge). Isso torna a gestão de cota opaca justamente para quem tem responsabilidade sobre ela.
**Tarefas**
- [ ] Decidir e documentar no \`@callback\` do \`Taina.Ybira.Behaviour\` se admin pode listar toda a lixeira da tekoa
- [ ] Se sim: ajustar \`list_trash/2\` para que quando \`scope.ava.role == :admin\`, a query não filtre por \`ava_id\`
- [ ] Adicionar teste cobrindo o cenário de admin listando lixeira de outro membro
- [ ] Considerar adicionar um campo de resposta indicando o dono do arquivo (\`ava_id\` ou \`ava.username\`) para facilitar a auditoria
_Levantado durante a revisão do `#151` por `@zoedsoupe` — $PR_URL_" \
--field labels='["engineering"]' \
--field assignees='["zoedsoupe"]' \
--jq '"Created: #\(.number) — \(.title)\n\(.html_url)"'
echo ""
# Issue 3: Purge em batch
gh api repos/taina-labs/taina/issues \
--method POST \
--field title="[Ybira] Otimizar purge_deleted_files/1 com deleção em batch no banco" \
--field body="**Descrição**
A função \`purge_deleted_files/1\` hoje percorre os arquivos expirados um a um, abrindo uma transação individual no banco para cada registro deletado. Para uma comunidade com uma lixeira grande (ex.: 500–1.000 arquivos acumulados em 30 dias), isso significa 500–1.000 transações sequenciais numa única execução do worker — o que pode ser lento num hardware modesto como um Raspberry Pi 5.
A remoção física do disco (\`File.rm/1\`) continuará sendo individual (não dá para fazer em batch), mas a parte de banco e a atualização de cota podem ser feitas de forma muito mais eficiente com um \`Repo.delete_all\` agrupado.
**Tarefas**
- [ ] Refatorar \`purge_deleted_files/1\` para usar \`Repo.delete_all\` com \`select: f\` para recuperar os registros deletados de uma vez
- [ ] Calcular o ajuste de cota por tekoa em batch (agrupar por \`tekoa_id\` e somar \`file_size_bytes\`)
- [ ] Manter o \`Enum.each\` apenas para a remoção física dos arquivos em disco
- [ ] Atualizar o teste em \`PurgeTrashTest\` para cobrir um cenário com múltiplos arquivos de tekoас diferentes
- [ ] Medir (mesmo que informalmente) a diferença de tempo com e sem o batch num volume maior de registros
_Levantado durante a revisão do `#151` por `@zoedsoupe` — $PR_URL_" \
--field labels='["engineering"]' \
--field assignees='["zoedsoupe"]' \
--jq '"Created: #\(.number) — \(.title)\n\(.html_url)"'
echo ""
# Issue 4: EBML/Matroska ambiguidade no MimeDetector
gh api repos/taina-labs/taina/issues \
--method POST \
--field title="[Ybira] MimeDetector classifica arquivos .mkv como video/webm (EBML ambiguidade)" \
--field body="**Descrição**
O \`Taina.Ybira.MimeDetector\` identifica o magic byte \`0x1A 0x45 0xDF 0xA3\` como \`video/webm\`. Esse magic byte é, na verdade, o cabeçalho **EBML genérico**, usado tanto pelo WebM quanto pelo **Matroska (.mkv)**. O resultado: arquivos \`.mkv\` são retornados com MIME type \`video/webm\`.
Para o MVP isso não é um problema de segurança (ambos os formatos estão na allowlist e são vídeos legítimos), mas pode causar confusão na interface: o usuário envia um \`.mkv\` e o sistema registra \`video/webm\`. Se futuramente a interface exibir o tipo do arquivo ou tomar decisões baseadas no MIME, isso pode gerar comportamentos inesperados.
A distinção entre WebM e Matroska exige ler o campo \`DocType\` dentro do header EBML (que vem logo após os primeiros bytes), o que implica ler um pouco mais do arquivo.
**Tarefas**
- [ ] Avaliar se a distinção WebM × Matroska é relevante para os casos de uso da plataforma no MVP
- [ ] Se sim: implementar leitura do campo \`DocType\` EBML para distinguir \`webm\` → \`video/webm\` de \`matroska\` → \`video/x-matroska\`
- [ ] Adicionar teste no \`MimeDetectorTest\` cobrindo um arquivo com header EBML Matroska
- [ ] Atualizar a allowlist se \`video/x-matroska\` for aceito
_Levantado durante a revisão do `#151` por `@zoedsoupe` — $PR_URL_" \
--field labels='["engineering"]' \
--field assignees='["zoedsoupe"]' \
--jq '"Created: #\(.number) — \(.title)\n\(.html_url)"'
echo ""
# Issue 5: `@doc` em Scope.for_ava/1
gh api repos/taina-labs/taina/issues \
--method POST \
--field title="[Maraca] Documentar exigência de preload em Scope.for_ava/1" \
--field body="**Descrição**
A função \`Scope.for_ava/1\` exige que a struct \`Ava\` recebida tenha a associação \`tekoa\` **já precarregada**. Sem isso, a função levanta um \`FunctionClauseError\` em tempo de execução — um erro que pode ser difícil de rastrear, especialmente para quem está adicionando um novo call site pela primeira vez.
Hoje só existe um call site (\`FileController\`), onde \`get_session_user/1\` já garante o preload. Mas conforme a plataforma cresce e novos contextos chamam \`Scope.for_ava/1\`, a ausência de documentação explícita sobre esse requisito vira uma armadilha silenciosa.
**Tarefas**
- [ ] Adicionar \`@doc\` em \`Scope.for_ava/1\` documentando explicitamente que \`ava.tekoa\` precisa estar precarregada
- [ ] Incluir exemplo de uso correto (com \`Repo.preload\`) na docstring
- [ ] Considerar adicionar um \`@spec\` mais preciso ou uma guard clause que dê uma mensagem de erro mais clara quando o preload estiver faltando
_Levantado durante a revisão do `#151` por `@zoedsoupe` — $PR_URL_" \
--field labels='["documentation"]' \
--field assignees='["zoedsoupe"]' \
--jq '"Created: #\(.number) — \(.title)\n\(.html_url)"'Length of output: 10199 🏁 Script executed: #!/bin/bash
PR_URL="https://github.com/taina-labs/taina/pull/151"
# Issue 1: Permission.changeset granted_by_id auditoria
echo '{
"title": "[Maraca] Trilha de auditoria: garantir que granted_by_id nunca seja nil em Permission",
"labels": ["engineering"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nAtualmente, o `Permission.changeset/2` faz `cast` e `validate_required` apenas para `:resource_id`, `:resource_type`, `:action` e `:ava_id`. O campo `granted_by_id` — que representa **quem concedeu a permissão** — é injetado externamente via `put_change/3` por quem chama o changeset.\n\nIsso funciona nos caminhos conhecidos (`insert_grant` e `approve_access_request`), mas deixa uma brecha silenciosa: qualquer novo caminho que chame `Permission.changeset/2` diretamente pode persistir uma permissão sem `granted_by_id`, comprometendo a trilha de auditoria sem levantar nenhum erro.\n\nNum sistema orientado à soberania e transparência das comunidades, saber **quem** concedeu uma permissão é tão importante quanto saber **o que** foi concedido.\n\n**Tarefas**\n\n- [ ] Criar um `grant_changeset/2` específico que inclua `granted_by_id` e `tekoa_id` no `cast` e no `validate_required`\n- [ ] Migrar `insert_grant` e `approve_access_request` para usar `grant_changeset/2`\n- [ ] Adicionar teste cobrindo a rejeição de uma permissão sem `granted_by_id`\n- [ ] Revisar se há outros call sites que chamam `Permission.changeset` diretamente\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'
echo ""
# Issue 2: list_trash/2 visibilidade admin
echo '{
"title": "[Ybira] Admins não conseguem ver a lixeira de outros membros da comunidade",
"labels": ["engineering"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nA função `list_trash/2` filtra os arquivos deletados pelo `ava_id` do scope, ou seja, cada usuário enxerga **apenas sua própria lixeira**. Isso é o comportamento correto para membros comuns, mas deixa o admin sem visibilidade sobre os arquivos deletados de toda a tekoa.\n\nO problema prático: se a cota de armazenamento de uma comunidade está alta e o owner ou admin quer entender o motivo, não consegue auditar quais arquivos estão na lixeira ocupando espaço (pois os bytes não são devolvidos no soft delete — só no purge). Isso torna a gestão de cota opaca justamente para quem tem responsabilidade sobre ela.\n\n**Tarefas**\n\n- [ ] Decidir e documentar no `@callback` do `Taina.Ybira.Behaviour` se admin pode listar toda a lixeira da tekoa\n- [ ] Se sim: ajustar `list_trash/2` para que quando `scope.ava.role == :admin`, a query não filtre por `ava_id`\n- [ ] Adicionar teste cobrindo o cenário de admin listando lixeira de outro membro\n- [ ] Considerar adicionar um campo de resposta indicando o dono do arquivo (`ava_id` ou `ava.username`) para facilitar a auditoria\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'
echo ""
# Issue 3: Purge em batch
echo '{
"title": "[Ybira] Otimizar purge_deleted_files/1 com deleção em batch no banco",
"labels": ["engineering"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nA função `purge_deleted_files/1` hoje percorre os arquivos expirados um a um, abrindo uma transação individual no banco para cada registro deletado. Para uma comunidade com uma lixeira grande (ex.: 500–1.000 arquivos acumulados em 30 dias), isso significa 500–1.000 transações sequenciais numa única execução do worker — o que pode ser lento num hardware modesto como um Raspberry Pi 5.\n\nA remoção física do disco (`File.rm/1`) continuará sendo individual (não dá para fazer em batch), mas a parte do banco e a atualização de cota podem ser feitas de forma muito mais eficiente com um `Repo.delete_all` agrupado.\n\n**Tarefas**\n\n- [ ] Refatorar `purge_deleted_files/1` para usar `Repo.delete_all` com `select: f` para recuperar os registros deletados de uma vez\n- [ ] Calcular o ajuste de cota por tekoa em batch (agrupar por `tekoa_id` e somar `file_size_bytes`)\n- [ ] Manter o `Enum.each` apenas para a remoção física dos arquivos em disco\n- [ ] Atualizar o teste em `PurgeTrashTest` para cobrir um cenário com múltiplos arquivos de tekoас diferentes\n- [ ] Medir (mesmo que informalmente) a diferença de tempo com e sem o batch num volume maior de registros\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'
echo ""
# Issue 4: EBML/Matroska ambiguidade
echo '{
"title": "[Ybira] MimeDetector classifica arquivos .mkv como video/webm (ambiguidade EBML)",
"labels": ["engineering"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nO `Taina.Ybira.MimeDetector` identifica o magic byte `0x1A 0x45 0xDF 0xA3` como `video/webm`. Esse magic byte é, na verdade, o cabeçalho **EBML genérico**, usado tanto pelo WebM quanto pelo **Matroska (.mkv)**. O resultado: arquivos `.mkv` são retornados com MIME type `video/webm`.\n\nPara o MVP isso não é um problema de segurança (ambos os formatos estão na allowlist e são vídeos legítimos), mas pode causar confusão na interface: o usuário envia um `.mkv` e o sistema registra `video/webm`. Se futuramente a interface exibir o tipo do arquivo ou tomar decisões baseadas no MIME, isso pode gerar comportamentos inesperados.\n\nA distinção entre WebM e Matroska exige ler o campo `DocType` dentro do header EBML (logo após os primeiros bytes), o que implica ler um pouco mais do arquivo.\n\n**Tarefas**\n\n- [ ] Avaliar se a distinção WebM × Matroska é relevante para os casos de uso da plataforma no MVP\n- [ ] Se sim: implementar leitura do campo `DocType` EBML para distinguir `webm` → `video/webm` de `matroska` → `video/x-matroska`\n- [ ] Adicionar teste no `MimeDetectorTest` cobrindo um arquivo com header EBML Matroska\n- [ ] Atualizar a allowlist se `video/x-matroska` for aceito\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'
echo ""
# Issue 5: `@doc` em Scope.for_ava/1
echo '{
"title": "[Maraca] Documentar exigência de preload em Scope.for_ava/1",
"labels": ["documentation"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nA função `Scope.for_ava/1` exige que a struct `Ava` recebida tenha a associação `tekoa` **já precarregada**. Sem isso, a função levanta um `FunctionClauseError` em tempo de execução — um erro que pode ser difícil de rastrear, especialmente para quem está adicionando um novo call site pela primeira vez.\n\nHoje só existe um call site (`FileController`), onde `get_session_user/1` já garante o preload. Mas conforme a plataforma cresce e novos contextos chamam `Scope.for_ava/1`, a ausência de documentação explícita sobre esse requisito vira uma armadilha silenciosa.\n\n**Tarefas**\n\n- [ ] Adicionar `@doc` em `Scope.for_ava/1` documentando explicitamente que `ava.tekoa` precisa estar precarregada\n- [ ] Incluir exemplo de uso correto (com `Repo.preload`) na docstring\n- [ ] Considerar adicionar um `@spec` mais preciso ou uma guard clause que dê uma mensagem de erro mais clara quando o preload estiver faltando\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'Length of output: 8852 🏁 Script executed: #!/bin/bash
# Issue 2: list_trash/2 visibilidade admin
echo '{
"title": "[Ybira] Admins não conseguem ver a lixeira de outros membros da comunidade",
"labels": ["engineering"],
"assignees": ["zoedsoupe"],
"body": "**Descrição**\n\nA função `list_trash/2` filtra os arquivos deletados pelo `ava_id` do scope, ou seja, cada usuário enxerga **apenas sua própria lixeira**. Isso é o comportamento correto para membros comuns, mas deixa o admin sem visibilidade sobre os arquivos deletados de toda a tekoa.\n\nO problema prático: se a cota de armazenamento de uma comunidade está alta e o owner ou admin quer entender o motivo, não consegue auditar quais arquivos estão na lixeira ocupando espaço (pois os bytes não são devolvidos no soft delete — só no purge). Isso torna a gestão de cota opaca justamente para quem tem responsabilidade sobre ela.\n\n**Tarefas**\n\n- [ ] Decidir e documentar no `@callback` do `Taina.Ybira.Behaviour` se admin pode listar toda a lixeira da tekoa\n- [ ] Se sim: ajustar `list_trash/2` para que quando `scope.ava.role == :admin`, a query não filtre por `ava_id`\n- [ ] Adicionar teste cobrindo o cenário de admin listando lixeira de outro membro\n- [ ] Considerar adicionar um campo de resposta indicando o dono do arquivo (`ava_id` ou `ava.username`) para facilitar a auditoria\n\n_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_"
}' | gh api repos/taina-labs/taina/issues --method POST --input - --jq '"✅ #\(.number) — \(.title)\n \(.html_url)"'Length of output: 1939 🏁 Script executed: #!/bin/bash
gh issue create \
--repo taina-labs/taina \
--title "[Ybira] Admins não conseguem ver a lixeira de outros membros da comunidade" \
--label "engineering" \
--assignee "zoedsoupe" \
--body "$(cat <<'EOF'
**Descrição**
A função `list_trash/2` filtra os arquivos deletados pelo `ava_id` do scope, ou seja, cada usuário enxerga **apenas sua própria lixeira**. Isso é o comportamento correto para membros comuns, mas deixa o admin sem visibilidade sobre os arquivos deletados de toda a tekoa.
O problema prático: se a cota de armazenamento de uma comunidade está alta e o owner ou admin quer entender o motivo, não consegue auditar quais arquivos estão na lixeira ocupando espaço (pois os bytes não são devolvidos no soft delete — só no purge). Isso torna a gestão de cota opaca justamente para quem tem responsabilidade sobre ela.
**Tarefas**
- [ ] Decidir e documentar no `@callback` do `Taina.Ybira.Behaviour` se admin pode listar toda a lixeira da tekoa
- [ ] Se sim: ajustar `list_trash/2` para que quando `scope.ava.role == :admin`, a query não filtre por `ava_id`
- [ ] Adicionar teste cobrindo o cenário de admin listando lixeira de outro membro
- [ ] Considerar adicionar um campo de resposta indicando o dono do arquivo (`ava_id` ou `ava.username`) para facilitar a auditoria
_Levantado durante a revisão do `#151` por `@zoedsoupe` — https://github.com/taina-labs/taina/pull/151_
EOF
)"Length of output: 1778 🏁 Script executed: #!/bin/bash
printf '{"title":"[Ybira] Admins nao conseguem ver a lixeira de outros membros da comunidade","labels":["engineering"],"assignees":["zoedsoupe"],"body":"**Descricao**\\n\\nA funcao `list_trash/2` filtra os arquivos deletados pelo `ava_id` do scope, ou seja, cada usuario enxerga **apenas sua propria lixeira**. Isso e o comportamento correto para membros comuns, mas deixa o admin sem visibilidade sobre os arquivos deletados de toda a tekoa.\\n\\nO problema pratico: se a cota de armazenamento de uma comunidade esta alta e o owner ou admin quer entender o motivo, nao consegue auditar quais arquivos estao na lixeira ocupando espaco (pois os bytes nao sao devolvidos no soft delete, so no purge). Isso torna a gestao de cota opaca justamente para quem tem responsabilidade sobre ela.\\n\\n**Tarefas**\\n\\n- [ ] Decidir e documentar no `@callback` do `Taina.Ybira.Behaviour` se admin pode listar toda a lixeira da tekoa\\n- [ ] Se sim: ajustar `list_trash/2` para que quando `scope.ava.role == :admin`, a query nao filtre por `ava_id`\\n- [ ] Adicionar teste cobrindo o cenario de admin listando lixeira de outro membro\\n- [ ] Considerar adicionar um campo de resposta indicando o dono do arquivo (`ava_id` ou `ava.username`) para facilitar a auditoria\\n\\n_Levantado durante a revisao do `#151` por `@zoedsoupe` - https://github.com/taina-labs/taina/pull/151_"}' \
| gh api repos/taina-labs/taina/issues --method POST --input - --jq '"#\(.number) \(.html_url)"'Length of output: 1574 🏁 Script executed: #!/bin/bash
# Issue 3: Purge em batch
printf '{"title":"[Ybira] Otimizar purge_deleted_files/1 com delecao em batch no banco","labels":["engineering"],"assignees":["zoedsoupe"],"body":"**Descricao**\\n\\nA funcao `purge_deleted_files/1` hoje percorre os arquivos expirados um a um, abrindo uma transacao individual no banco para cada registro deletado. Para uma comunidade com uma lixeira grande (ex.: 500-1.000 arquivos acumulados em 30 dias), isso significa 500-1.000 transacoes sequenciais numa unica execucao do worker, o que pode ser lento num hardware modesto como um Raspberry Pi 5.\\n\\nA remocao fisica do disco (`File.rm/1`) continuara sendo individual, mas a parte do banco e a atualizacao de cota podem ser feitas de forma muito mais eficiente com um `Repo.delete_all` agrupado.\\n\\n**Tarefas**\\n\\n- [ ] Refatorar `purge_deleted_files/1` para usar `Repo.delete_all` com `select: f` para recuperar os registros deletados de uma vez\\n- [ ] Calcular o ajuste de cota por tekoa em batch (agrupar por `tekoa_id` e somar `file_size_bytes`)\\n- [ ] Manter o `Enum.each` apenas para a remocao fisica dos arquivos em disco\\n- [ ] Atualizar o teste em `PurgeTrashTest` para cobrir um cenario com multiplos arquivos de tekoас diferentes\\n- [ ] Medir (mesmo que informalmente) a diferenca de tempo com e sem o batch num volume maior de registros\\n\\n_Levantado durante a revisao do `#151` por `@zoedsoupe` - https://github.com/taina-labs/taina/pull/151_"}' \
| gh api repos/taina-labs/taina/issues --method POST --input - --jq '"#\(.number) \(.html_url)"'Length of output: 1658 🏁 Script executed: #!/bin/bash
# Issue 4: EBML/Matroska ambiguidade
printf '{"title":"[Ybira] MimeDetector classifica arquivos .mkv como video/webm (ambiguidade EBML)","labels":["engineering"],"assignees":["zoedsoupe"],"body":"**Descricao**\\n\\nO `Taina.Ybira.MimeDetector` identifica o magic byte `0x1A 0x45 0xDF 0xA3` como `video/webm`. Esse magic byte e, na verdade, o cabecalho **EBML generico**, usado tanto pelo WebM quanto pelo **Matroska (.mkv)**. O resultado: arquivos `.mkv` sao retornados com MIME type `video/webm`.\\n\\nPara o MVP isso nao e um problema de seguranca (ambos os formatos estao na allowlist e sao videos legitimos), mas pode causar confusao na interface: o usuario envia um `.mkv` e o sistema registra `video/webm`. Se futuramente a interface exibir o tipo do arquivo ou tomar decisoes baseadas no MIME, isso pode gerar comportamentos inesperados.\\n\\nA distincao entre WebM e Matroska exige ler o campo `DocType` dentro do header EBML (logo apos os primeiros bytes), o que implica ler um pouco mais do arquivo.\\n\\n**Tarefas**\\n\\n- [ ] Avaliar se a distincao WebM x Matroska e relevante para os casos de uso da plataforma no MVP\\n- [ ] Se sim: implementar leitura do campo `DocType` EBML para distinguir `webm` em `video/webm` de `matroska` em `video/x-matroska`\\n- [ ] Adicionar teste no `MimeDetectorTest` cobrindo um arquivo com header EBML Matroska\\n- [ ] Atualizar a allowlist se `video/x-matroska` for aceito\\n\\n_Levantado durante a revisao do `#151` por `@zoedsoupe` - https://github.com/taina-labs/taina/pull/151_"}' \
| gh api repos/taina-labs/taina/issues --method POST --input - --jq '"#\(.number) \(.html_url)"'Length of output: 1738 🏁 Script executed: #!/bin/bash
# Issue 5: `@doc` em Scope.for_ava/1
printf '{"title":"[Maraca] Documentar exigencia de preload em Scope.for_ava/1","labels":["documentation"],"assignees":["zoedsoupe"],"body":"**Descricao**\\n\\nA funcao `Scope.for_ava/1` exige que a struct `Ava` recebida tenha a associacao `tekoa` **ja precarregada**. Sem isso, a funcao levanta um `FunctionClauseError` em tempo de execucao, um erro que pode ser dificil de rastrear, especialmente para quem esta adicionando um novo call site pela primeira vez.\\n\\nHoje so existe um call site (`FileController`), onde `get_session_user/1` ja garante o preload. Mas conforme a plataforma cresce e novos contextos chamam `Scope.for_ava/1`, a ausencia de documentacao explicita sobre esse requisito vira uma armadilha silenciosa.\\n\\n**Tarefas**\\n\\n- [ ] Adicionar `@doc` em `Scope.for_ava/1` documentando explicitamente que `ava.tekoa` precisa estar precarregada\\n- [ ] Incluir exemplo de uso correto (com `Repo.preload`) na docstring\\n- [ ] Considerar adicionar um `@spec` mais preciso ou uma guard clause que de uma mensagem de erro mais clara quando o preload estiver faltando\\n\\n_Levantado durante a revisao do `#151` por `@zoedsoupe` - https://github.com/taina-labs/taina/pull/151_"}' \
| gh api repos/taina-labs/taina/issues --method POST --input - --jq '"#\(.number) \(.html_url)"'Length of output: 1438
Cada issue segue o template de desenvolvimento/documentação do projeto, com descrição contextualizada do problema, impacto prático e checklist de tarefas. Backlinkadas para o |
Problema
A aplicação Taina precisava avançar a fase 2 do Ybira (RFC 002 §5): adicionar suporte a pastas hierárquicas, lixeira, paginação eficiente, validação segura de tipos de arquivo por assinatura binária, downloads com HTTP Range, controle de cotas e jobs agendados para purge de itens deletados.
Solução
deleted_at(arquivo mantido em disco); restore_file e list_trash adicionados; PurgeTrash (Oban cron 03:00) remove permanentemente itens com >30 dias.Explicação
Escolhas técnicas visam segurança e escalabilidade: soft delete permite recuperação imediata enquanto purge periódico recupera espaço e ajusta cota; keyset melhora desempenho em grandes listas; detecção por magic bytes evita confiança em extensões fáceis de falsificar; HTTP Range economiza banda em downloads; Behaviour isola contrato do domínio; e whitelist explícita no Repo evita vazamento silencioso de dados.